Skip to content

Conversation

@willemneal
Copy link
Contributor

@willemneal willemneal commented Aug 7, 2025

This also simplifies Upgrade to depend on Ownable.

A continuation of #334

@netlify
Copy link

netlify bot commented Aug 7, 2025

Deploy Preview for delightful-dieffenbachia-2097e0 canceled.

Name Link
🔨 Latest commit e322e81
🔍 Latest deploy log https://app.netlify.com/projects/delightful-dieffenbachia-2097e0/deploys/68b8afe5fc07120008da0496

@willemneal willemneal force-pushed the feat/use_contracttrait_pr branch from 746fa43 to 56f0362 Compare August 8, 2025 18:02
@willemneal willemneal force-pushed the feat/use_contracttrait_pr branch from 5d8bb5a to 2869014 Compare August 12, 2025 15:43
@willemneal willemneal force-pushed the feat/use_contracttrait_pr branch from 2869014 to 9bc9170 Compare August 12, 2025 17:14
@willemneal willemneal marked this pull request as ready for review August 12, 2025 21:49
@ozgunozerk
Copy link
Collaborator

I'll post some of the discussion points below that happened through DM. So, that @brozorec and others can chime in.

Comment on lines -46 to +45
type ContractType = AllowList;
type Impl = AllowList;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me:

ContractType is changed to Impl. We decided to keep it as ContractType in our previous discussion

Willem:

My branch of the contracttraits adds the default impl redirect, but uses Impl . What I wanted was to be consistent with contracttraits which used the associated type.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think ContractType is more self-explanatory than an abstract name such as Impl. I'd prefer to keep it as ContractType

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about ContractImpl? Type is also broad and it already is a type. What it is is the implementation of the contract so ContractImpl seems appropriate to me.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is already a type

Yeah I give you that :D

I don't think Impl keyword fits in here.

What it is is the implementation of the contract so ContractImpl seems appropriate to me.

I disagree. It is the associated type. The implementation of the contract is happening elsewhere, this is just the indicator type. I can go with Contract alone, agree that Type is redundant, but I don't think Impl fits here.


#[contractimpl]
impl FungibleBurnable for ExampleContract {
type Impl = AllowList;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me:

Why have we added ContractType to every trait? It was not required back then for FungibleAllowList, FungibleBurnable, AccessControl, etc.

Willem:

The idea was that I wanted an actual default implementation for each. This way devs could replace it with their own and opens up to the trait extensions which I use an another branch.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are specific reasons that we didn't want to provide default implementations for some modules, like AccessControl. This was a deliberate choice because the functions that will be implemented for the contract are critical w.r.t security, and providing defaults will effectively result in people using the default rather than carefully thinking about their business logic and how to configure AccessControl accordingly.

The reason for the existence of the ContractType trait, is to provide overrides across different types of contracts. For Allowlist, Burnable, AccessControl, there are no overrides needed for their interface.

I'd say this change is contradicting with 2 critical design choices of our library.


// Mint initial supply to the admin
Base::mint(e, &admin, initial_supply);
Self::internal_mint(e, admin, initial_supply);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me:

internal_mint is a new addition?

Willem:

mint was public but added an internal_mint to the FT trait with #[internal]. This way if you you wanted to swap of the default implementation it would be guaranteed to have it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I fully get it, could you elaborate on that a bit more @willemneal ?

Comment on lines -27 to +26
let (receiver, amount) = client.get_royalty_info(&token_id, &1000);
let (receiver, amount) = client.royalty_info(&token_id, &1000);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me:

get_royalty_info is renamed to royalty_info, debatable…

Willem:

It wasn’t renamed because royalty_info was part of the trait and thus exposed already. So there were two functions that did the same thing. So I’m open to renaming.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that sounds good! This is not a rename for the storage or interface, but a rename to the specific example contract implementation.

/// * topics - `["disallow", user: Address]`
/// * data - `[]`
fn disallow_user(e: &Env, user: Address, operator: Address);
fn disallow_user(e: &Env, user: &soroban_sdk::Address, operator: &soroban_sdk::Address);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we using the full path here &soroban_sdk::Address? Address is already imported above from soroban_sdk?

Comment on lines -61 to +59
pub trait NonFungibleEnumerable: NonFungibleToken<ContractType = Enumerable> {
#[contracttrait(add_impl_type = true)]
pub trait NonFungibleEnumerable {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it still work in mutually exclusive way with Consecutive for example?

What I mean is, trait bound is removed: pub trait NonFungibleEnumerable: NonFungibleToken<ContractType = Enumerable>

and, this trait bound was allowing us to get mutual exclusivity across incompatible contract types. More concretely, one cannot implement both Enumerable and Consecutive for the same contract.

Copy link
Collaborator

@brozorec brozorec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@willemneal great work 👌 This approach is viable and I think it makes the library more ergonomic.

There is the question about the unimplemented function (cf. one of the comments) that's a blocker for me atm.

/// * topics - `["role_granted", role: Symbol, account: Address]`
/// * data - `[caller: Address]`
fn grant_role(e: &Env, caller: Address, account: Address, role: Symbol);
fn grant_role(e: &Env, caller: &Address, account: &Address, role: &soroban_sdk::Symbol);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why some of the params are fully-qualified and others are not? What's the benefit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The contracttrait macro will generate the code and this basically means copy/pasting the types. So without this it means you will need to import them. Which isn't too bad but could be confusing for devs.

Comment on lines +91 to +120
fn enforce_admin_auth(e: &Env) {
let Some(admin) = Self::get_admin(e) else {
soroban_sdk::panic_with_error!(e, AccessControlError::AdminNotSet);
};
admin.require_auth();
}

fn ensure_role(e: &Env, caller: &soroban_sdk::Address, role: &soroban_sdk::Symbol) {
if Self::has_role(e, caller, role).is_none() {
soroban_sdk::panic_with_error!(e, AccessControlError::Unauthorized);
}
}

fn ensure_if_admin_or_admin_role(
e: &Env,
caller: &soroban_sdk::Address,
role: &soroban_sdk::Symbol,
) {
let is_admin = match Self::get_admin(e) {
Some(admin) => caller == &admin,
None => false,
};
let is_admin_role = match Self::get_role_admin(e, role) {
Some(admin_role) => Self::has_role(e, caller, &admin_role).is_some(),
None => false,
};
if !is_admin && !is_admin_role {
soroban_sdk::panic_with_error!(e, AccessControlError::Unauthorized);
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are default implementations for those internals, why do we re-impl them here?

Comment on lines +128 to +143
#[internal]
fn only_owner(e: &soroban_sdk::Env) {
let Some(owner) = Self::get_owner(e) else {
panic_with_error!(e, OwnableError::OwnerNotSet);
};
owner.require_auth()
}

#[internal]
fn enforce_owner_auth(e: &soroban_sdk::Env) -> Address {
let Some(owner) = Self::get_owner(e) else {
panic_with_error!(e, OwnableError::OwnerNotSet);
};
owner.require_auth();
owner
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we don't need them both

Comment on lines +48 to +50
fn allow_user(e: &Env, user: &Address, _operator: &Address) {
Self::allow_user_no_auth(e, user);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I commented out this fn and also the fn in the fungible-allowlist example + the FungibleAllowList trait in mod.rs doesn't provide a default impl, but everything still compiles 🤔

I was expecting it would scream about the unimplemented function, but that's not the case, which can be a serious issue for sensitive functions like this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants