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
use crate::modules::AccessControl;
use crate::utils::Error;
use odra::{
    contract_env,
    prelude::{format, string::String},
    types::{event::OdraEvent, Address, Balance, U256},
    Mapping, Sequence, Variable,
};
use odra_modules::erc721::events::Transfer;
use odra_modules::erc721::{
    erc721_base::Erc721Base,
    extensions::erc721_metadata::{Erc721Metadata, Erc721MetadataExtension},
    Erc721,
};

/// A unique token id.
pub type TokenId = U256;
/// A distinct Uniform Resource Identifier (URI) for a token.
pub type TokenUri = String;

/// NFT module used by DAO.
#[odra::module(events = [Transfer])]
pub struct DaoNft {
    core: Erc721Base,
    metadata: Erc721MetadataExtension,
    access_control: AccessControl,
    tokens: Mapping<Address, Option<TokenId>>,
    id_gen: Sequence<TokenId>,
    total_supply: Variable<Balance>,
}

#[odra::module]
impl DaoNft {
    delegate! {
        to self.access_control {
            /// Changes the ownership of the contract. Transfers ownership to the `owner`.
            /// Only the current owner is permitted to call this method.
            /// [`Read more`](AccessControl::propose_new_owner())
            pub fn propose_new_owner(&mut self, owner: Address);
            /// Accepts the new owner proposition. This can be called only by the proposed owner.
            /// [`Read more`](AccessControl::accept_new_owner())
            pub fn accept_new_owner(&mut self);
            /// Adds a new address to the whitelist.
            /// [`Read more`](AccessControl::add_to_whitelist())
            pub fn add_to_whitelist(&mut self, address: Address);
            /// Remove address from the whitelist.
            /// [`Read more`](AccessControl::remove_from_whitelist())
            pub fn remove_from_whitelist(&mut self, address: Address);
            /// Checks whether the given address is added to the whitelist.
            /// [`Read more`](AccessControl::is_whitelisted()).
            pub fn is_whitelisted(&self, address: Address) -> bool;
            /// Returns the address of the current owner.
            /// [`Read more`](AccessControl::get_owner()).
            pub fn get_owner(&self) -> Option<Address>;
        }

        to self.metadata {
            /// Returns a descriptive name for a collection of tokens in this contract.
            pub fn name(&self) -> String;
            /// Gets an abbreviated name for tokens in this contract.
            pub fn symbol(&self) -> String;
            /// Returns a URI prefix that is used by all the assets.
            pub fn base_uri(&self) -> TokenUri;
        }

        to self.core {
            /// Returns the address of the owner of the token.
            ///
            /// If the given `token_id` does not exist the None value is returned.
            pub fn owner_of(&self, token_id: &TokenId) -> Address;
            /// Returns the number of tokens owned by `owner`.
            pub fn balance_of(&self, owner: &Address) -> U256;
        }
    }

    /// Module constructor.
    ///
    /// Initializes modules. Sets the deployer as the owner.
    ///
    /// See [Erc721MetadataExtension](Erc721MetadataExtension::init()), [AccessControl](AccessControl::init())
    pub fn init(&mut self, name: String, symbol: String, base_uri: TokenUri) {
        let deployer = contract_env::caller();
        self.metadata.init(name, symbol, base_uri);
        self.access_control.init(deployer);
    }

    /// Returns the total number of tokens.
    pub fn total_supply(&self) -> Balance {
        self.total_supply.get_or_default()
    }

    /// Returns the token id for a given `address`.
    ///
    /// If the `owner` does not own any token the None value is returned.
    pub fn token_id(&self, address: Address) -> Option<TokenId> {
        self.tokens.get(&address).unwrap_or(None)
    }

    /// Returns a distinct Uniform Resource Identifier (URI) for a given asset.
    pub fn token_uri(&self, token_id: TokenId) -> TokenUri {
        if !self.core.exists(&token_id) {
            contract_env::revert(Error::TokenDoesNotExist)
        }
        format!("{}{}", self.metadata.base_uri(), token_id)
    }

    /// Creates a new token with the next id and transfers it to a new owner.
    /// Increments the total supply and the balance of the `to` address.
    ///
    /// # Note
    /// Only whitelisted addresses are permitted to call this function.
    ///
    /// Each user is entitled to own only one token.
    ///
    /// # Errors
    /// * [`UserAlreadyOwnsToken`](Error::UserAlreadyOwnsToken) if the `to` address
    /// already owns a token.
    ///
    /// # Events
    /// * [`Transfer`] event when minted successfully.
    pub fn mint(&mut self, to: Address) {
        // Check if the caller is whitelisted
        self.access_control.ensure_whitelisted();
        self.assert_does_not_own_token(&to);

        let token_id = self.id_gen.next_value();

        // Mint token
        if self.core.exists(&token_id) {
            contract_env::revert(Error::TokenAlreadyExists)
        }
        self.core.balances.add(&to, U256::one());
        self.total_supply.add(Balance::one());
        self.core.owners.set(&token_id, Some(to));

        self.tokens.set(&to, Some(token_id));

        Transfer {
            from: None,
            to: Some(to),
            token_id,
        }
        .emit();
    }

    /// Burns a token with the given id. Decrements the balance of the token owner
    /// and decrements the total supply.
    ///
    /// # Errors
    /// * [`NotWhitelisted`](crate::utils::Error::NotWhitelisted) if the caller
    /// is not whitelisted.
    ///
    /// # Events
    /// * [`Transfer`] event when burnt successfully.
    pub fn burn(&mut self, owner: Address) {
        self.access_control.ensure_whitelisted();
        let token_id = self.token_id(owner);

        if let Some(token_id) = token_id {
            self.core.balances.subtract(&owner, U256::from(1));
            self.core.owners.set(&token_id, None);
            self.core.clear_approval(&token_id);
            self.total_supply.subtract(Balance::from(1));
            self.tokens.set(&owner, None);

            Transfer {
                from: Some(owner),
                to: None,
                token_id,
            }
            .emit();
        }
    }
}

impl DaoNft {
    fn assert_does_not_own_token(&self, address: &Address) {
        if self.tokens.get(address).is_some() {
            contract_env::revert(Error::UserAlreadyOwnsToken)
        }
    }
}