Skip to content

Commit 6868fa4

Browse files
committed
standards: add DRC20 primitive and reference
1 parent bbf1c85 commit 6868fa4

12 files changed

Lines changed: 1473 additions & 1 deletion

File tree

Makefile

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

standards/Cargo.lock

Lines changed: 14 additions & 0 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
@@ -1,6 +1,7 @@
11
[workspace]
22
members = [
33
"dusk-contract-standards",
4+
"examples/drc20_roles_pausable",
45
"examples/multisig_controller",
56
]
67

standards/dusk-contract-standards/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ pub mod auth;
4040
pub mod core;
4141
pub mod governance;
4242
pub mod security;
43+
pub mod token;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
//! DRC20 event payloads.
8+
9+
use bytecheck::CheckBytes;
10+
use rkyv::{Archive, Deserialize, Serialize};
11+
12+
use crate::core::Principal;
13+
14+
/// Transfer event topic.
15+
pub const TRANSFER_TOPIC: &str = "drc20/transfer";
16+
/// Approval event topic.
17+
pub const APPROVAL_TOPIC: &str = "drc20/approval";
18+
19+
/// Transfer event.
20+
#[derive(
21+
Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq,
22+
)]
23+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24+
#[archive_attr(derive(CheckBytes))]
25+
pub struct Transfer {
26+
/// Sender.
27+
pub from: Principal,
28+
/// Recipient.
29+
pub to: Principal,
30+
/// Amount.
31+
pub amount: u64,
32+
}
33+
34+
/// Approval event.
35+
#[derive(
36+
Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq,
37+
)]
38+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39+
#[archive_attr(derive(CheckBytes))]
40+
pub struct Approval {
41+
/// Owner.
42+
pub owner: Principal,
43+
/// Spender.
44+
pub spender: Principal,
45+
/// Amount.
46+
pub amount: u64,
47+
}
48+
49+
impl dusk_forge::ContractEvent for Transfer {
50+
const TOPICS: &'static [&'static str] = &[TRANSFER_TOPIC];
51+
}
52+
53+
impl dusk_forge::ContractEvent for Approval {
54+
const TOPICS: &'static [&'static str] = &[APPROVAL_TOPIC];
55+
}
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
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+
//! Optional DRC20 policy helpers.
8+
9+
use alloc::collections::BTreeMap;
10+
use alloc::vec::Vec;
11+
12+
use bytecheck::CheckBytes;
13+
use rkyv::{Archive, Deserialize, Serialize};
14+
15+
use crate::core::{error, Principal};
16+
17+
/// Supply cap policy.
18+
#[derive(
19+
Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq,
20+
)]
21+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22+
#[archive_attr(derive(CheckBytes))]
23+
pub struct SupplyCap {
24+
cap: u64,
25+
}
26+
27+
impl SupplyCap {
28+
/// Creates a supply cap.
29+
pub const fn new(cap: u64) -> Self {
30+
Self { cap }
31+
}
32+
33+
/// Returns the cap.
34+
pub const fn cap(&self) -> u64 {
35+
self.cap
36+
}
37+
38+
/// Sets a new cap. The new cap must not be below current supply.
39+
pub fn set_cap(&mut self, current_supply: u64, cap: u64) {
40+
if cap < current_supply {
41+
panic!("DRC20: cap below current supply");
42+
}
43+
self.cap = cap;
44+
}
45+
46+
/// Returns remaining mintable supply.
47+
pub const fn remaining(&self, current_supply: u64) -> u64 {
48+
self.cap.saturating_sub(current_supply)
49+
}
50+
51+
/// Panics unless minting `amount` keeps supply within cap.
52+
pub fn assert_mint(&self, current_supply: u64, amount: u64) {
53+
let next = current_supply.checked_add(amount).expect(error::OVERFLOW);
54+
if next > self.cap {
55+
panic!("DRC20: cap exceeded");
56+
}
57+
}
58+
}
59+
60+
/// Vote or accounting checkpoint.
61+
#[derive(
62+
Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq,
63+
)]
64+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65+
#[archive_attr(derive(CheckBytes))]
66+
pub struct Checkpoint {
67+
/// Block height, timestamp, epoch, or app-selected timepoint.
68+
pub key: u64,
69+
/// Value at that timepoint.
70+
pub value: u64,
71+
}
72+
73+
/// Ordered checkpoint trace.
74+
#[derive(Clone, Debug, Default, PartialEq, Eq)]
75+
pub struct Checkpoints {
76+
checkpoints: Vec<Checkpoint>,
77+
}
78+
79+
impl Checkpoints {
80+
/// Creates an empty trace.
81+
pub const fn new() -> Self {
82+
Self {
83+
checkpoints: Vec::new(),
84+
}
85+
}
86+
87+
/// Returns all checkpoints.
88+
pub fn entries(&self) -> &[Checkpoint] {
89+
&self.checkpoints
90+
}
91+
92+
/// Returns latest value or zero.
93+
pub fn latest(&self) -> u64 {
94+
self.checkpoints
95+
.last()
96+
.map(|checkpoint| checkpoint.value)
97+
.unwrap_or(0)
98+
}
99+
100+
/// Writes a checkpoint.
101+
pub fn push(&mut self, key: u64, value: u64) {
102+
if let Some(last) = self.checkpoints.last_mut() {
103+
if key < last.key {
104+
panic!("Checkpoints: non-monotonic key");
105+
}
106+
if key == last.key {
107+
last.value = value;
108+
return;
109+
}
110+
}
111+
self.checkpoints.push(Checkpoint { key, value });
112+
}
113+
114+
fn assert_can_push(&self, key: u64) {
115+
if let Some(last) = self.checkpoints.last() {
116+
if key < last.key {
117+
panic!("Checkpoints: non-monotonic key");
118+
}
119+
}
120+
}
121+
122+
/// Returns value at or before `key`.
123+
pub fn get_at(&self, key: u64) -> u64 {
124+
let mut low = 0usize;
125+
let mut high = self.checkpoints.len();
126+
while low < high {
127+
let mid = (low + high) / 2;
128+
if self.checkpoints[mid].key <= key {
129+
low = mid + 1;
130+
} else {
131+
high = mid;
132+
}
133+
}
134+
if low == 0 {
135+
0
136+
} else {
137+
self.checkpoints[low - 1].value
138+
}
139+
}
140+
}
141+
142+
/// DRC20 voting-unit checkpoint store.
143+
///
144+
/// Composing contracts call `move_units` after mint, burn, and transfer hooks
145+
/// when they want historical vote/accounting queries.
146+
#[derive(Clone, Debug, Default)]
147+
pub struct VotingUnits {
148+
accounts: BTreeMap<Principal, Checkpoints>,
149+
total_supply: Checkpoints,
150+
}
151+
152+
impl VotingUnits {
153+
/// Creates empty voting-unit state.
154+
pub const fn new() -> Self {
155+
Self {
156+
accounts: BTreeMap::new(),
157+
total_supply: Checkpoints::new(),
158+
}
159+
}
160+
161+
/// Returns latest account units.
162+
pub fn latest_votes(&self, account: Principal) -> u64 {
163+
self.accounts
164+
.get(&account)
165+
.map(Checkpoints::latest)
166+
.unwrap_or(0)
167+
}
168+
169+
/// Returns historical account units.
170+
pub fn past_votes(&self, account: Principal, timepoint: u64) -> u64 {
171+
self.accounts
172+
.get(&account)
173+
.map(|trace| trace.get_at(timepoint))
174+
.unwrap_or(0)
175+
}
176+
177+
/// Returns latest total units.
178+
pub fn latest_total_supply(&self) -> u64 {
179+
self.total_supply.latest()
180+
}
181+
182+
/// Returns historical total units.
183+
pub fn past_total_supply(&self, timepoint: u64) -> u64 {
184+
self.total_supply.get_at(timepoint)
185+
}
186+
187+
/// Writes an account checkpoint directly.
188+
pub fn write_votes(
189+
&mut self,
190+
account: Principal,
191+
timepoint: u64,
192+
value: u64,
193+
) {
194+
self.accounts
195+
.entry(account)
196+
.or_default()
197+
.push(timepoint, value);
198+
}
199+
200+
/// Moves units between accounts, minting from `None` or burning to `None`.
201+
pub fn move_units(
202+
&mut self,
203+
from: Option<Principal>,
204+
to: Option<Principal>,
205+
amount: u64,
206+
timepoint: u64,
207+
) {
208+
if amount == 0 || from == to {
209+
return;
210+
}
211+
212+
let from_next = if let Some(from) = from {
213+
let current = self.latest_votes(from);
214+
if current < amount {
215+
panic!("VotingUnits: insufficient units");
216+
}
217+
Some((from, current - amount))
218+
} else {
219+
None
220+
};
221+
222+
let to_next = if let Some(to) = to {
223+
let current = self.latest_votes(to);
224+
let next = current.checked_add(amount).expect(error::OVERFLOW);
225+
Some((to, next))
226+
} else {
227+
None
228+
};
229+
230+
let total_next = match (from, to) {
231+
(None, Some(_)) => Some(
232+
self.latest_total_supply()
233+
.checked_add(amount)
234+
.expect(error::OVERFLOW),
235+
),
236+
(Some(_), None) => {
237+
let current = self.latest_total_supply();
238+
if current < amount {
239+
panic!("VotingUnits: total supply underflow");
240+
}
241+
Some(current - amount)
242+
}
243+
_ => None,
244+
};
245+
246+
if let Some((from, _)) = from_next {
247+
self.assert_can_write_votes(from, timepoint);
248+
}
249+
if let Some((to, _)) = to_next {
250+
self.assert_can_write_votes(to, timepoint);
251+
}
252+
if total_next.is_some() {
253+
self.total_supply.assert_can_push(timepoint);
254+
}
255+
256+
if let Some((from, value)) = from_next {
257+
self.write_votes(from, timepoint, value);
258+
}
259+
if let Some((to, value)) = to_next {
260+
self.write_votes(to, timepoint, value);
261+
}
262+
if let Some(value) = total_next {
263+
self.total_supply.push(timepoint, value);
264+
}
265+
}
266+
267+
fn assert_can_write_votes(&self, account: Principal, timepoint: u64) {
268+
if let Some(trace) = self.accounts.get(&account) {
269+
trace.assert_can_push(timepoint);
270+
}
271+
}
272+
}

0 commit comments

Comments
 (0)