Skip to content

Support Associated Types in Supertraits #287

@0xNeshi

Description

@0xNeshi

Supertraits and associated types are useful when defining inheritance relationships and overriding "parent" functions. Unfortunately, neither are fully supported by the SDK yet.

The below implementation does not work:

trait IErc20 {
    type Error;

    fn transfer(&mut self, to: Address, value: U256) -> Result<bool, Self::Error>;

    // other funcs...
}

trait IErc20Burnable: IErc20 {
    fn burn(&mut self, value: U256) -> Result<(), Self::Error>;
}

// ...

sol! {
    #[derive(Debug)]
    error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);
    // other errors...
}

#[derive(SolidityError, Debug)]
pub enum Error {
    InsufficientBalance(ERC20InsufficientBalance),
    // other errors...
}

#[storage]
#[entrypoint]
struct Erc20BurnableExample {
    balances: StorageMap<Address, StorageU256>,
    allowances: StorageMap<Address, StorageMap<Address, StorageU256>>,
    total_supply: StorageU256,
}

#[public]
#[implements(IErc20<Error = Error>, IErc20Burnable<Error = Error>)] // setting assoc. type for `IErc20Burnable` is necessary
impl Erc20BurnableExample {}

#[public]
impl IErc20 for Erc20BurnableExample {
    type Error = Error;

    fn transfer(&mut self, to: Address, value: U256) -> Result<bool, Self::Error> {
        // implementation
        Ok(true)
    }

    // other func implementations...
}

#[public]
impl IErc20Burnable for Erc20BurnableExample { 
    // ^^^^^^^^^^^^^^^
    // value of the associated type `Error` in `IErc20` must be specified
    fn burn(&mut self, value: U256) -> Result<(), Self::Error> {
        // implementation
        Ok(())
    }
}

The issue seems to be that public at some point generates a dyn IErc20Burnable that needs to actually be dyn IErc20Burnable<Error = Error>.
But if we set that in any way, we get a compiler error:

#[public]
impl IErc20Burnable<Error = Error> for Erc20BurnableExample {
                            // ^^^^^^^^^^^^^^
                            // associated item constraints are not allowed here

or:

#[public]
impl IErc20Burnable for Erc20BurnableExample {
   type Error = Error;
// ^^^^^^^^^^^^^^^^^
// `type Error` is not a member of trait `IErc20Burnable`

Consider that the #[public] macro should also work with traits having multiple supertraits with associated types. Currently this errors out:

trait IErc20 {
    type Error;

    fn transfer(
        &mut self,
        to: Address,
        value: U256,
    ) -> Result<bool, Self::Error>;

    // other funcs...
}

trait IPausable {
    type Error;

    fn pause(&mut self) -> Result<(), Self::Error>;
}

trait IErc20Burnable: IErc20 + IPausable {
    type Error: From<<Self as IErc20>::Error> + From<<Self as IPausable>::Error>;

    fn burn(
        &mut self,
        value: U256,
    ) -> Result<(), <Self as IErc20Burnable>::Error>;
}

// other defs and impls...

#[public]
// ^^^^^^
// the value of the associated types `Error` in `IErc20`, `Error` in `IPausable` must be specified
// consider introducing a new type parameter, adding `where` constraints using the fully-qualified path to the associated types
impl IErc20Burnable for Erc20BurnableExample {
    type Error = Error;

    fn burn(
        &mut self,
        value: U256,
    ) -> Result<(), <Self as IErc20Burnable>::Error> {
        // implementation
        Ok(())
    }
}

A dev-friendly solution would be to have an additional attribute to set under #[public] macro that sets the appropriate associated types for supertraits:

#[public]
#[associated_types(IErc20: Error = Error)]
impl IErc20Burnable for Erc20BurnableExample {
    fn burn(&mut self, value: U256) -> Result<(), Self::Error> {
        Ok(())
    }
}

If there are multiple supertraits, with potentially multiple associated types, they could all be comma-separated:

#[public]
#[associated_types(
    ISuperTrait: Key = String, Value = Vec<u8>,
    AnotherSupertrait: ExpiryTime = u64
)]
impl ChildTrait for Contract {
    // ...
}

This is just a suggestion, maybe there are more dev-friendly ways to implement this. I'm afraid verbosity is the price of Rust's type-safety.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions