paint-brush
Solidity 0.8.19: Diving Into User-Defined Operators for User-Defined Value Typesby@zartaj
335 reads
335 reads

Solidity 0.8.19: Diving Into User-Defined Operators for User-Defined Value Types

by Md Zartaj AfserOctober 9th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Learn about this gasless abstraction of value types in solidity and the operators feature for them.
featured image - Solidity 0.8.19: Diving Into User-Defined Operators for User-Defined Value Types
Md Zartaj Afser HackerNoon profile picture

Solidity 0.8.19 came up with quite a few changes and upgrades. In this post, I’ll talk about user-defined operators for user-defined value types. (I’ll use UDO and UDVT ).

Before learning about UDOs, we need to look into UDVTs.

What are User-Defined Value Types

UDVTs are gasless abstractions of value types over the pre-defined value types in solidity. This was introduced in solidity 0.8.8. In simpler terms, we can call them aliases for other value types. The core reason behind introducing this was to facilitate more strict definitions of variables.

Quick Example

For example, our contract stores two types of addresses, one for buyers and the other for sellers. We already know that under the hood, both of these variables will be of type address; however,


  • what if we want to make them different to avoid any confusion or intermingling of different concepts?

  • What if we want the data types to be more descriptive about the value it’s storing?


Before UDVTs were introduced, this goal was attained using structs. Below is an example.

The four functions in this code are used to wrap or unwrap the given data type into or from the defined data type.


I am sure you have seen this type of usage in some contracts. However, as we know, structs are reference types; they’ll use memory, costing more gas than just using uint. This is where UDVTs are being used. They are simple extracts that don’t cost gas.


Now, as far as the syntax is concerned, we can define UDVTs like type A is B where A is the extracted value type, can also call an alias for B which is the underlying type that can be uint, address, etc. Once we declare these value types, we get two attached methods with them, wrap and unwrap, A.wrap(value) can be used to convert an underlying type to the newly created value type, and is used for the vice versa.


Let’s see this in the code.

This was a demonstration related to the previous code snippet however, you might have realized that we don’t need to define the wrapping/unwrapping functions explicitly because solidity gives wrap/unwrap functions. So, if we talk about the usage of these functions, below is another code snippet.


// SPDX-License-Identifier: GPL-3.0 
  pragma solidity ^0.8.8;

// Represent an 18 decimal, 256-bit wide fixed point type// using a user-defined value type.type UFixed is uint256;

/// A minimal library to do fixed point operations on UFixed.
uint256 constant multiplier = 10\*\*18;  

/// Adds two UFixed numbers. Reverts on overflow,  
/// relying on checked arithmetic on uint256.  
function add(UFixed a, UFixed b) internal pure returns (UFixed) {  
    return UFixed.wrap(UFixed.unwrap(a) + UFixed.unwrap(b));  
}  

/// Multiplies UFixed and uint256. Reverts on overflow,  
/// relying on checked arithmetic on uint256.  
function mul(UFixed a, uint256 b) internal pure returns (UFixed) {  
    return UFixed.wrap(UFixed.unwrap(a) \* b);  
}  

/// Take the floor of a UFixed number.  
/// @return the largest integer that does not exceed \`a\`.  
function floor(UFixed a) internal pure returns (uint256) {  
    return UFixed.unwrap(a) / multiplier;  
}  

/// Turns a uint256 into a UFixed of the same value.  
/// Reverts if the integer is too large.  
function toUFixed(uint256 a) internal pure returns (UFixed) {  
    return UFixed.wrap(a \* multiplier);  
}  

}


We can conclude that UDVTs are not something that affects the logic of the contract but a kind of syntactical sugar that makes our code clearer and readable. PRB math library, for example, which is a library used to tackle the floating number issue in solidity, uses UDVTs to define different types of floating numbers.


Now, the problem arises when we try to use arithmetic operations on UDVTs. As you can see in the above example, we have to unwrap the passed variables to use built-in operators. This is where UDOs are introduced.

What are User Defined Operators?

UDOs are built using the two built-in features of solidity, i.e., built-in operators and using … for ….

UDOs are an extended version of using for. To recall, using for is used to:


  1. Attaching all Library functions to a data type.using LibrayName for TypeName

  2. Attaching library functions to all the data types. using LibrayName for *

  3. Attaching specific library functions or free functions to any specific type.using {LibrayName.FunctionName, FreeFunctionName} for TypeName


Now, UDO comes into play and defines the fourth type of using for statement. Something like this:


using {FunctionName as OperatorSign} for UDVTName global;


This syntax facilitates attaching a function as an operator sign, the same as attaching a library function. The difference is we can use an operator sign instead of using the default way of calling the function. This will be clearer by looking at the code example below.


There are some rules while using User Defined Operators.


  • They can only be defined as free functions(functions defined at the file level).
  • The free functions should be pure.
  • Can only be defined at global using for directive, i.e., it can not be inside a contract but at a file level.
  • User-defined Operator can only be attached to the UDVTs and not to the underlying default value types.
  • They can only be defined for a particular type, i.e., they won’t work with different UDVTs in the same function.
  • At last, while attaching the operators to any UDVT, only these operator signs can be used: &, |, ^, ~, +, -, *, /, %, ==, !=, <, <=, >, >=.

Let’s now look at a code example.



// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

//The types are defined at the file level
type Float is uint256;
type unFloat is uint256;

//"using for" statements are written at the global directive
//not inside a contract
using {add as +} for Float global;
using {multiply as *} for Float global;
using {divide as /} for Float global;

//These are pure free functions, which is a requirement
///@notice Each function works with only one type. 
    function add(Float a, Float b) pure returns (Float) {
        return Float.wrap(Float.unwrap(a) + Float.unwrap(b));
    }

    function multiply(Float a, Float b) pure returns (Float) {
        return Float.wrap(Float.unwrap(a) * Float.unwrap(b));

    }

    function divide(Float a, Float b) pure returns (Float) {
        return Float.wrap(Float.unwrap(a) / Float.unwrap(b));

    }

//Using the attached operators inside a contract
contract UDO {
    
    Float cent = Float.wrap(100);
    Float decimal = Float.wrap(1e18);

 //The multiplication and division using operators is only possible
 // because we attached these particular operators' sign to the relevant functions
    function takePercent(Float _amount, Float totalAmount)
        external
        view
        returns (Float)
    {
        return (_amount * cent * decimal)/(totalAmount);

    }
}


Attaching any function to any UDVT is independent of binding the same function to it. This means that we can’t call the free function with UDVTs in a similar fashion as a library function like add(a,b)or a.add(b) when we are binding them as an operator, but obviously, we can do so when they are attached as functions. Let’s look at the example below.


// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

type Float is uint256;
type unFloat is uint256;

//      binding and attaching
using {multiply as *,multiply} for Float global;
using {divide as /} for Float global;

    function multiply(Float a, Float b) pure returns (Float) {
        return Float.wrap(Float.unwrap(a) * Float.unwrap(b));
    }

    function divide(Float a, Float b) pure returns (Float) {
      return Float.wrap(Float.unwrap(a) / Float.unwrap(b));
    }

contract UDO {
    
    Float cent = Float.wrap(100);
    Float decimal = Float.wrap(1e18)

    function takePercent(Float _amount, Float totalAmount)
        external
        view
        returns (Float)
    {
        // return (_amount * cent * decimal)/(totalAmount);
        return      (_amount.multiply(decimal.multiply(cent))).divide(totalAmount);
       //In this line Multiply will work but divide will throw an error, because 
       // we haven't bound the divide function to Float.    
}
}

The signs we used for particular functions are not strictly needed; we could’ve used / instead of + with the add function, and the / would work as the add function. However, that will cause unwanted confusion, so we won’t do that.

What are the use cases?

As discussed earlier, the UDVTs can be used to prevent any kind of type mistakes and provide a better understanding of the logic. UDOs increase the usability of the same by making it more accessible and familiar.


The best usage of these are math libraries, where we can have multiple types of floating numbers and much more.


That’s it for this article. Join us with all your solidity questions/confusions HERE.


Also published here.