Skip to content

Conversation

@hanzel98
Copy link
Contributor

@hanzel98 hanzel98 commented Mar 26, 2025

What?

  • Added the new MultiTokenPeriodEnforcer. Unified enforcer contract that allows a user to grant access for multiple tokens (both ERC20 and native) with a single signature. All tokens share the same delegation hash, so revoking or disabling the delegation applies to every token. Based on ERC20PeriodTransferEnforcer and NativeTokenPeriodTransferEnforcer.

Why?

  • ERC20PeriodTransferEnforcer and NativeTokenPeriodTransferEnforcer contracts work well when a delegation involves only a single token. However, when a user needs to delegate transfers for multiple tokens, having separate enforcers forces multiple signatures and complicates the user experience.

How?

  • A single enforcer that combines both, and allows multiple tokens

Gas and design

While creating this enforcer an idea for improving gas cost was implemented, this idea consisted on hashing the msg.sender, delegationHash, and token to store the index on the state and avoid looping everytime through the token list.
Gas test comparisons were made to evaluate that idea. We were able to see improvements, but the highest gas savings come from using large token lists. After talking to the team internally, we found that 5 tokens will be the most common list, so because of that we stick to the idea of looping through the tokens. The loop doesn't process all the tokens it only loops to find a token match.

Below are the gas savings for comparison.

The gas is only for the function getTermsInfo, which looks for the match in the token. I compared the current version (No store on state) against the version that uses the hash+cache

  • Using 100 tokens:
    No store on state: 48881 constant
    Storing index on state: 87930, first time storing, but then it goes down to gas used: 2072, constant forever, it pays up on the 2nd run.

  • Using 20 tokens:
    No store on state: getTermsInfo gas used: 10801 constant
    Storing index on state: 54570, first time storing, but then it goes down to gas used: 2072, constant forever, it pays up on the 7th run.

  • Using 10 tokens:
    No store on state: getTermsInfo gas used: 6041 constant
    Storing index on state: 50400 first time storing, but then it goes down to gas used: 2072, constant forever, it pays up on the 13th run.

  • Using 5 tokens:
    No store on state: getTermsInfo gas used: 3661 constant
    Storing index on state: 48315, first time storing, but then it goes down to gas used: 2072, constant forever, it pays up on the 30th run.

  • Using 3 tokens:
    No store on state: getTermsInfo gas used: 2709 constant
    Storing index on state: 47481 first time storing, but then it goes down to gas used: 2072 constant forever, it pays up on the 72th run.

  • Using 2 tokens:
    No store on state: getTermsInfo gas used: 2233 constant
    Storing index on state: 47064 first time storing, but then it goes down to gas used: 2072 constant forever, it pays up on the 281th run.

@hanzel98 hanzel98 requested a review from a team as a code owner March 26, 2025 19:24
@hanzel98 hanzel98 marked this pull request as draft March 26, 2025 19:24
@hanzel98 hanzel98 self-assigned this Mar 26, 2025
@hanzel98 hanzel98 marked this pull request as ready for review April 1, 2025 14:49
@hanzel98 hanzel98 requested a review from McOso April 1, 2025 14:49
@hanzel98
Copy link
Contributor Author

Just for history context of this hash+cache idea

struct TokenIndex {
        bool isSaved;
        uint256 index;
    }    
    
    mapping(bytes32 => TokenIndex) public tokenIndex;


    function getTermsInfo(
        bytes calldata _terms,
        address _token,
        bytes32 _delegationHash
    )
        public
        returns (uint256 periodAmount_, uint256 periodDuration_, uint256 startDate_)
    {
        uint256 termsLength_ = _terms.length;
        require(termsLength_ != 0 && termsLength_ % 116 == 0, "MultiTokenPeriodEnforcer:invalid-terms-length");

        bytes32 indexKey_ = keccak256(abi.encodePacked(msg.sender, _delegationHash, _token));
        TokenIndex storage tokenIndex_ = tokenIndex[indexKey_];
        if (tokenIndex_.isSaved) return _getTerms(_terms, tokenIndex_.index * 116);

        // Iterate over the byte offset directly in increments of 116 bytes.
        uint256 index_;
        for (uint256 offset_ = 0; offset_ < termsLength_;) {
            // Extract token address from the first 20 bytes.
            address token_ = address(bytes20(_terms[offset_:offset_ + 20]));
            if (token_ == _token) {
                tokenIndex[indexKey_] = TokenIndex({ isSaved: true, index: index_ });
                return _getTerms(_terms, offset_);
            }
            unchecked {
                ++index_;
                offset_ += 116;
            }
        }
        revert("MultiTokenPeriodEnforcer:token-config-not-found");
    }

McOso
McOso previously approved these changes Apr 10, 2025
@hanzel98 hanzel98 force-pushed the feat/multi-token-period-enforcer branch from 0fa8f5d to 49818d4 Compare April 10, 2025 17:52
@hanzel98 hanzel98 merged commit 0205e0d into main Apr 10, 2025
4 checks passed
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