3

I'm trying to understand why assembly for this sum is required? Why not use + and % ?

    _structHash = keccak256(
        abi.encode(
            _blockhash,
            totalAddresses,
            gasLeft,
            _externalRandomNumber
        )
    );
    _randomNumber  = uint256(_structHash);
    assembly {_randomNumber := add(mod(_randomNumber, _maxNumber),1)} // why?
    winningNumbers[0]=uint8(_randomNumber);
5
  • 1
    Which contract is that from? If they have a Github, please try asking there. And if you get an answer, please post back here :)
    – eth
    Apr 18, 2021 at 11:56
  • 1
    This comes from pancake swap lottery.
    – redigaffi
    Apr 18, 2021 at 16:43
  • 1
    OK. Usually it is better to include that information in the question. If they have a Github, that would be another place to try asking.
    – eth
    May 2, 2021 at 10:02
  • @eth, please see my answer!
    – Mila A
    Mar 5 at 17:48
  • 1
    @MilaA I did not run your code but like the approach you've taken. There might be other stuff at github.com/ethereum/solidity/issues?q=mod I've upvoted and it's nice redigaffi has accepted the answer.
    – eth
    Mar 8 at 8:41

1 Answer 1

1

First of all, I'd like to explain what they [the PancakeSwap team] intend to accomplish with these lines...


MOD: This opcode calculates the remainder of the second-to-top element on the stack divided by the top element and pushes the result onto the stack. It is used for performing modulo operations.

So MOD is basically an equivalent of %.

The reason PancakeSwap Lottery contract utilizes the modulo function for the pseudo number generation is that the algorithm it uses is a slightly tailored mixed congruential pseudo-number generation method.

And if you want the generated number to remain within specific integer constraints (e.g., < 1,000), you need to use the modulo function to trim the number without clearing its "randomness".

add(mod(_randomNumber, _maxNumber),1) <-- add(..., 1) is used here to handle the cases when the remainder = 0, apparently because they want the random number to always remain > 0.

Although adding 1 IN EVERY CASE feels unnatural to me, because when the remainder is > 0, why would I want to add 1 to it? That feels unfair to the winner who is the remainder (when remainder is > 0)... 🙂


I wrote a small test to check whether there were any practical constraints that made the developers use assembly instead of high-level syntax:

pragma solidity ^0.6.0;

contract Test {
    uint8 _maxNumber;
    uint256 _pseudoRandomNumber = 2345100000000000000000000000000000000000000000000000000000000000000000000;

    constructor() public {
        _maxNumber = uint8(1000);
    }

    function modulo() external view returns (uint256 result) {
        return (_pseudoRandomNumber % _maxNumber) + 1;
    }
}

... it compiles well and returns 33.

WHICH IS NOT WHAT WE WOULD EXPECT IT TO RETURN, RIGHT?

result_modulo_33


But comparing that result (33) with the result of the assembly operations surprised me even more...

pragma solidity ^0.6.0;

contract Test {

    function modulo() external view returns (uint256 result) {
        uint8 _maxNumber = uint8(1000);
        uint256 _pseudoRandomNumber = 2345100000000000000000000000000000000000000000000000000000000000000000000;
        assembly {
            result := add(mod(_pseudoRandomNumber, _maxNumber), 1)
        }
    }
}

assembly-mod-result-is-quite-unexpected-actually

The result is 1. That is correct!


And here is a unified test, for clarity:

pragma solidity ^0.6.0;

contract Test {
    function moduloPlain() external view returns (uint256 result) {
        uint8 _maxNumber = uint8(1000);
        uint256 _pseudoRandomNumber = 2345200000000000000000000000000000000000000000000000000000000000000000003;
        result = (_pseudoRandomNumber % _maxNumber) + 1;
    }

    function moduloAssembly() external view returns (uint256 result) {
        uint8 _maxNumber = uint8(1000);
        uint256 _pseudoRandomNumber = 2345200000000000000000000000000000000000000000000000000000000000000000003;
        assembly {
            result := add(mod(_pseudoRandomNumber, _maxNumber), 1)
        }
    }
}

That results in:

result-3


As we can tell from these tests, the lower-level assembly mod and add functions give more accurate and precision-friendly results than the higher-level % and + operators do for when the input integers are of different bit sizes.


And, while I cannot answer the question why is there a HUGE difference between the high-level and low-level interpreted snippet results, that is the REASON why the PancakeSwap team used the assembly for these math operations for their Lottery contract.

Though one curious thing I noticed is the following...

pragma solidity ^0.6.0;

contract Test {
    function moduloPlain() external view returns (uint256 result) {
        result = (2345200000000000000000000000000000000000000000000000000000000000000000003 % 1000) + 1;
    }

    function moduloAssembly() external view returns (uint256 result) {
        uint8 _maxNumber = uint8(1000);
        uint256 _pseudoRandomNumber = 2345200000000000000000000000000000000000000000000000000000000000000000003;
        assembly {
            result := add(mod(_pseudoRandomNumber, _maxNumber), 1)
        }
    }
}

... Gives an absolutely correct, and — what's more important — an expected, result:

curious-result-number-4


To conclude, it looks like the culprit is something in how higher-level Solidity treats integers of different bit sizes when calculating the modulo function.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.