Recently, USDG came up as one of the topmost stablecoins in the Solana ecosystem, and it is equally gaining gradual adoption in the EVM. If you are a developer or security researcher, there are quite a lot of exciting things you will want to know about USDG. For instance, even though it’s an abstraction of legacy ERC20, it has a couple of modifications, it’s denylisting bit calculation is also one for the books. Do you want to build a similar product and wants to understand the nitty-gritty? Or you want to find criticals in the codebase and are looking for technical chops to start? Keep reading. The project has 3 components: Let’s take it one after the other. Diving into the ERC20Upgradeable_V2 The ERC20Upgradeable_V2 is a succession of the initial ERC20Upgradeable contract with a zero-day vulnerability; we will cover the details of this implementation flaw later on. By the way, we will only cover the functions that are necessary and jump over the overly straightforward ones. Imports and State Variables The contract uses some upgradeability imports from OpenZeppelin such as: You will get to know how these libraries are utilized in the contract, as in their specific purposes, as we move on. Of course, the created usual ERC20 mappings [like balances and allowances] and variables like _totalSupply, _name, and _symbol all of which are made private as anyone would expect. But this is where you need to pay attention: how the BALANCE_DENY_FLAG_SET_MASK and MAX_ALLOWED_SUPPLY are created. Put on your thinking caps. Explaining the BALANCE_DENY_FLAG_SET_MASK Design and Implementation For you to fully understand what’s going on here, I have to refresh your mind on some basics of Computer Science, particularly bits. Bits are the smallest unit of data processing and storage. Bit are always been 0 and 1. On the other hand, a byte is a bunch of 8 bits. In line with that, a storage slot in the EVM stores 256 bits, which is an equivalent of 32 bytes. Hold that on one hand, we digressed, now, let’s go back to the denylisting variable. Hope you understand it to this point. Moving forward, you definitely know that data fill storage slots linearly. However, in cases where we need to manipulate slots and have more control, we can use masks – this should ring a bell if you remember bitwise operations well. This is what the developers of USDG did here: In plain language, denylisted objects will be pushed to the last bit of the binary for easier identification. By the way, have you wondered why we shift to 255 instead of 256? The reason is simple: 0 is an actual value in binary, so our bit started from 0. If you count 0 to 255, we have 256 numbers. Hope you get it. Well, I’ll now explain the entire design with a story: Imagine you are in a barrack with 255 rooms, with a guardroom at the 255th room. So anytime a soldier messes up, they switch their room from say R6 to R255 where they will be locked there for a long time. Why this design decision? Left for me, I would have simply created a boolean variable where I can simply denylist by saying denylisted = true or something similar. This is quite what any dev would have done, it’s the easiest and most straightforward technique you can think of. However, the USDG smart contract architect noticed a huge problem circle was facing – denylisting with other methods are quite costly, unnecessarily so. According to Alex Kroeger’s research, the blacklist functionality of USDC eventually cost approximately $4 million in 2022. This was absurd and equally unsustainable. Hence, from the onset, USDG took a different approach with regards to implementing denylisting. In short, the reason they used bitmask was to make the contract more efficient. Explaining the MAX_ALLOWED_SUPPLY Design and Implementation The same bitmask technique used above was used here, albeit with slight modifications. Here is a breakdown of what the devs did here: You might want to ask, “why not shift the bit to 254, so there will be no need to reduce it by 1 slot?” That’s really a brilliant observation. Just that Bitwise operations don’t work that way, especially in line with the intended design. If we quickly pull it up to 255, that will be the arbitrary highest bit in the slot, and that will backstab our intended design of keeping denylisted objects at 255. ERC20 Initializations This is an upgradeable contract, meaning we can initialize it without or with constructors. Thus, the name and symbol variables above were initialized. In each cases, they both have the onlyInitializing modifier which will prevent reset from blackhats. You might wonder, what’s the different between these two initialization techniques. The full gist is on OpenZeppelin docs, but I’ll do a quick abstraction. The latter can be overridden in child contracts, while the former is specifically for this contract itself. Native OZ ERC20 Functions For proper customization, some functions must be modified in the contract. This includes name and symbol. Let’s zoom in a bit on how the balanceOf was implemented: return _balances[account] & ~BALANCE_DENY_FLAG_SET_MASK; The instruction in this function is the balances of accounts along with their status of denylisting, whether they are or not, should be returned. This line is quite a marriage of bits, let’s take some time to break it down: The transfer, allowance, transferFrom and approve functions work normally in accordance with usual ERC20 implementation of OZ. Allowance Incrementation and Decrementation One of the banes of approvals is that things can go south and you wouldn’t be able to decrease or increase your approved tokens as you wish. More particularly, if you give a maximum allowance or you were compromised, you cannot reduce your allowance. In the increaseAllowance function, the first control implemented is that only the sender can give it. Secondly, the logic adds new value to the existing given allowance. Moving to the decreaseAllowance function, it checks, first of all, that