Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add erc4626 #1170

Open
wants to merge 111 commits into
base: main
Choose a base branch
from
Open

Conversation

andrew-fleming
Copy link
Collaborator

@andrew-fleming andrew-fleming commented Oct 1, 2024

WIP

Fixes #???

PR Checklist

  • Tests
  • Documentation
  • Added entry to CHANGELOG.md
  • Tried the feature on a public network

...still a WIP. The tests need to be refactored and improved. The idea was to make sure the logic works as expected so there's a standard (from the openzeppelin solidity tests) for any and all changes moving forward.

Some things to note and discuss:

Decimals

EIP4626 states at the end of the doc:

Although the convertTo functions should eliminate the need for any use of an EIP-4626 Vault’s decimals variable, it is still strongly recommended to mirror the underlying token’s decimals if at all possible, to eliminate possible sources of confusion and simplify integration across front-ends and for other off-chain users.

The OpenZeppelin solidity implementation checks for the underlying asset's tokens upon construction with a try/catch which defaults to 18 decimals if query fails. Given Starknet's current status with try/catch (✌️ dual dispatchers), this doesn't seem like a viable approach. If the vault is for an undeployed token, the deployment will fail. This PR proposes that the decimals (both underlying decimals and offset decimals) are explicitly defined by the contract through the ImmutableConfig.

Math

u512 Precision math for multiply and divide (u256_mul_div)

This PR leverages the corelib's wide_mul and u512_safe_div_rem_by_u256 for mathematical precision. This is set as a tmp solution and should be scrutinized further. More tests should be provided and we should look for ways to optimize.

Exponentiation

This PR requires exponentiation for converting to shares and assets. The current implementation is the brute force formula. We can definitely improve this.

This was added to the corelib (starkware-libs/cairo#6694). Will update when released

FeeConfigTrait

This PR proposes to utilize FeeConfigTrait (which is really like a hook) for contracts to integrate entry and exit fees for preview_ fns. The state-changing methods of ERC4626 rely on preview_ to determine the number of assets or shares to exchange.

Another approach that can reduce the verbosity of the traits/hooks is to have a single adjust_assets_or_shares function that accepts an ExchangeType.

    #[derive(Drop, Copy)]
    pub enum ExchangeType {
        Deposit,
        Withdraw,
        Mint,
        Redeem
    }

    pub trait ERC4626HooksTrait<TContractState> {
        (...)

        fn adjust_assets_or_shares(
            self: @ComponentState<TContractState>, exchange_type: ExchangeType, raw_amount: u256
        ) -> u256 {
            raw_amount
        }
    }

    /// Component method example
    fn preview_mint(self: @ComponentState<TContractState>, shares: u256) -> u256 {
        let raw_amount = self._convert_to_assets(shares, Rounding::Ceil);
        Hooks::adjust_assets_or_shares(self, ExchangeType::Deposit, raw_amount)
    }

The downside though is I think it's easy to misuse i.e.

#[starknet::contract]
mod Contract {
    (...)

    impl ERC4626HooksImpl of ERC4626Component::ERC4626HooksTrait<ContractState> {
        fn adjust_assets_or_shares(
            self: @ERC4626Component::ComponentState<ContractState>, exchange_type: ExchangeType, raw_amount: u256
        ) -> u256 {
            match exchange_type {
                ExchangeType::Mint => {
                    // do something
                },
                ExchangeType::Deposit => {
                    // do something
                },
                ExchangeType::Withdraw => {
                    // do something
                },
                ExchangeType::Redeem => {
                    // do something
                }
            }
        }
    }
}

IMO having a dedicated function for each exchange type is more difficult to mess up...but it's at the cost of some verbosity.

LimitsConfigTrait

This mirrors the FeeConfigTrait except that these target the limits on the max_ methods and return an Option so Option::None can point to the default. Same arguments apply for not having a single trait/hook with an ExchangeType parameter.

before_withdraw and after_deposit hooks

The before_withdraw and after_deposit hooks take inspiration from solmate's solidity implementation of erc4626. These hooks are where contracts can transfer fees calculated from the FeeConfigTrait in the implementing contract. See the Fees mock to see how this works in the proposed PR.

Copy link

codecov bot commented Oct 1, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 89.41%. Comparing base (0676415) to head (76e5e7a).
Report is 28 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1170      +/-   ##
==========================================
- Coverage   92.26%   89.41%   -2.86%     
==========================================
  Files          59       81      +22     
  Lines        1811     3496    +1685     
==========================================
+ Hits         1671     3126    +1455     
- Misses        140      370     +230     
Files with missing lines Coverage Δ
...s/token/src/erc20/extensions/erc4626/erc4626.cairo 100.00% <100.00%> (ø)
...token/src/erc20/extensions/erc4626/interface.cairo 100.00% <100.00%> (ø)
packages/utils/src/math.cairo 100.00% <100.00%> (ø)

... and 78 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 82d77be...76e5e7a. Read the comment docs.

Copy link
Member

@ericnordelo ericnordelo left a comment

Choose a reason for hiding this comment

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

Great work @andrew-fleming! Left some non-blocking suggestions.

asset_address: ContractAddress, shares: u256, recipient: ContractAddress,
) -> ERC4626ABIDispatcher {
let fee_basis_points = 500_u256; // 5%
let _value_without_fees = 10_000_u256;
Copy link
Member

Choose a reason for hiding this comment

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

Why the leading underscore?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No good reason. Fixed!

let fee_basis_points = 500_u256; // 5%
let _value_without_fees = 10_000_u256;
let _fees = (_value_without_fees * fee_basis_points) / 10_000_u256;
let _value_with_fees = _value_without_fees - _fees;
Copy link
Member

Choose a reason for hiding this comment

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

Is this value used?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nope, fixed!

packages/test_common/src/mocks/erc20.cairo Show resolved Hide resolved
packages/test_common/src/mocks/erc4626.cairo Outdated Show resolved Hide resolved
packages/test_common/src/mocks/erc4626.cairo Outdated Show resolved Hide resolved
packages/test_common/src/mocks/erc4626.cairo Outdated Show resolved Hide resolved
@andrew-fleming
Copy link
Collaborator Author

@ericnordelo when we have with_components finalized, I think we can make an erc4626 extension for basic entry/exit fees to make it easy to quickstart and improve dx. I suppose we could include it now, but it's a lot more boilerplate

Copy link
Collaborator

@immrsd immrsd left a comment

Choose a reason for hiding this comment

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

Good job, left a few minor suggestions

Comment on lines 366 to 372
fn max_withdraw(self: @ComponentState<TContractState>, owner: ContractAddress) -> u256 {
match Limit::withdraw_limit(self, owner) {
Option::Some(limit) => limit,
Option::None => {
let erc20_component = get_dep_component!(self, ERC20);
let owner_shares = erc20_component.balance_of(owner);
self._convert_to_assets(owner_shares, Rounding::Floor)
Copy link
Collaborator

Choose a reason for hiding this comment

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

What do you think, what amount should we return from max_withdraw call if there IS a limit of, let's say, 1000, and the owner's balance is 300? Without a limit the function will return 300, but if there is one, the value of 1000 will be returned.

Maybe we should return the balance if it's less than the limit? Same applies to redeem

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed! Good catch

packages/token/src/erc20/extensions/erc4626/erc4626.cairo Outdated Show resolved Hide resolved
packages/utils/src/math.cairo Outdated Show resolved Hide resolved
@ericnordelo ericnordelo linked an issue Jan 20, 2025 that may be closed by this pull request
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.

Cairo implementation of EIP-4626
4 participants