What it means and how it works
As with anything in crypto, projects that become popular enough will be used by scammers to steal from their users. Uniswap is no different. This article will explain the common ways that bad actors are using Uniswap as a tool for scams, how liquidity locking was created to prevent those scams, and the unique flexible approach Unipump.io has implemented.
When a market is created on Uniswap, the liquidity provider deposits an equal value of two tokens to create a pair. The depositor then receives a “pool token” in return, which is just an ERC20 representing their stake in the pool. The pool token may be redeemed at any time for an equal value amount of both tokens, based on the value at the time of redemption.
The problem that arose from the ability to remove liquidity at any time was that not all tokens are listed on other markets, then buyers get stuck with a bag and no way to sell it. In the case of scammers, they would create and promote a new token, and provide a large amount of liquidity to the pair, tricking buyers into thinking the token has a healthy market and possible future. Once enough people bought the new token, they would redeem all their liquidity tokens and receive all the ETH from the pool.
To prevent this, the concept of liquidity locking was created. The process is quite simple: the pool token’s movement is restricted by a time-based function. This means that once the restriction is set, they cannot be moved or redeemed until the pre-selected time has passed. This gives users more confidence in the markets they are involved in, because they know that for at least the next X months, the market must exist in some form. Since the code of a contract is generally public, standards have been created, along with simple tools that anyone can use to verify the process was done correctly. Currently Unicrypt is the most popular method for teams to use, and they provide a webpage where non-technical users can easily verify this was done.
If the Uniswap pair you are trading does not use liquidity locking and is not listed on any centralized exchanges, you are always faced with the direct risk of the tokens value going to zero at a moment’s notice.
Hidden Mint Functions
Unfortunately, liquidity locking is not enough. To get around this, scammers use another tactic: they mint more tokens. It does not matter if the pair you’re trading has liquidity locking if the team can create infinite tokens. While there are other tricky things scammers can do, we are going to focus on liquidity locking and hidden mint functions.
The ERC20 standard has a few functions, but we will focus on “mint”. You can search this term in any contract to see when it’s used, and you should do this for all new contracts you interact with. A simple search for this word can end up saving you a lot of money. And if you do this enough times, things that are clearly out of place will start to stand out to you.
This can be simpler, or more complex, depending on the contract you’re looking at. You will need to compare what the people who launched the token are saying to what’s in the code. A basic ERC20 token is very straight forward to check. But once you add in mechanics like staking, burn, or rebase, things can get messy quickly, and hiding things becomes easier.
How we did our liquidity lock
After reflection, it might have been a better idea for us to incorporate the Unicrypt locking system into our build. This would have allowed us to permissionlessly “borrow trust” from them to increase user confidence in our actions. But the reality is that from a code perspective, locking liquidity tokens is quite simple for an honest dev team. We wanted our locking to work in a very specific way that could be extended at various intervals but never reduced.
When our contract went live, half of the raised funds (roughly 125 ETH) were sent directly to Uniswap to create a market pair. Our contract has a function named createLiquidityCrisis, which is the only function that can do anything involving these pool tokens. Each time the function is called, it redeems 25% of the initial pool tokens that were used to create the pair. The first time it can be called is after one month. After that, the function will fail unless it has been three months since it was previously called.
This method creates a dynamic time lock and gives us the option of extending the lock by simply doing nothing. Our current minimum lockup period is ten months, but for every day that the function is available but does not get called, the overall time the liquidity is locked extends by a day. Our intention is to make sure the ETH/UPP market is always active while the bull run is going. (As we will continue to repeat, this product is only designed for bull markets.) Our goal is to have called the function 3 of the 4 possible times when the top hits, and still be locked out of the 4th call, so everyone can have some exit liquidity to panic sell into.
Time To Follow Along
Follow this guide CAREFULLY to confirm that our team cannot remove and redeem the liquidity tokens before the times specified. We will also outline how to check that we have no ability to mint extra tokens. Our process will be to identify the liquidity tokens in the contract, and then break down any functions that interact with them. Our goals are to confirm that there are restrictions on moving them or redeeming them, and that we have no ability to mint more tokens.
Don’t just read this and accept is as truth: that’s the same as not reading it and trusting us. Since we are a fully anonymous team, your default position should be that we are bad actors. Some of this will seem complex at first, but if you start following this guide for every shitcoin you buy, you’ll be good at it before you know it. Honestly, you shouldn’t even be clicking the links in this guide, you should type uniswap dot org in your address bar after confirming the correct address through MULTIPLE social feeds. Don’t trust, verify.
Head over to the verified contract on Etherscan here (https://etherscan.io/address/0xce25b4271cc4d937a7d9bf75b2068a7892b9961d#code). You should ALWAYS copy the code you’re checking into a proper code editor with syntax highlighting; one trick scammers can use is misnaming things. A recent example is a rug pull that used “modif1er” instead of the proper spelling. So always pay close attention and use searches to jump around the code instead of trusting your eyes. Don’t even trust yourself, verify.
We start with when the “init” function on line 1327 is called: this is a one-time function that initializes the system after the sale. When this function is called, it sets the addresses of the connected contracts. The require statements on lines 1335 and 1336 say this can only be called if those connected contract addresses have not been set. So calling it the first time sets them, and the fact that they are set prevents it from being called again, a simple one-time function.
Inside the init function on line 1338 the pair is first created, and it is referred to in this contract as “uniswapEthUppPair”. If we search the contract code we can see this is referenced six times, so we need to check where and how it’s being referenced. The first is when its assigned its name on line 1294. The next two are inside the init function, which we determined can never be called again, so that’s safe. The next two are inside the createLiquidityCrisis function, which is where all the action happens, so we’ll have to dig into the details. And lastly, at the bottom of the contract a check is done as part of a view function that prevents groups from interacting with the pool token all together: this is an extra safety measure.
Now we’re going to the Uniswap documentation here (https://uniswap.org/docs/v2/smart-contracts/router02/#removeliquidity) to confirm that the only direct way to remove liquidity involves calling functions that have the exact phrase “removeLiquidity” in the name. With this information, we can be 100% sure that if we do a search for “removeLiquidity” in the contract’s code, it will show us what functions need to be checked to confirm that liquidity is locked and for how long.
A search for “removeLiquidity” will return seven results. The first six results are part of the Uniswap interfaces. An interface is how contracts interact with each other. So we need to confirm this is the correct Uniswap interface and then we can ignore these results as they are just part of the integration, and were provided directly from Uniswap. Once we confirm that, we need to check what other functions access the ones in the interface, and what restrictions they have.
Start at line 407 in our code and compare it to the code given to us by Uniswap here (https://uniswap.org/docs/v2/smart-contracts/router01/#interface). Next, we compare the code starting at line 507 to the code provided by Uniswap here (https://uniswap.org/docs/v2/smart-contracts/router02/#interface). A good method is to use the copy button on the Uniswap page and paste the code into a new editor, put it beside our code, and check line by line that its an exact match. Due to the Etherscan verification process changing the spacing, it’s not as simple as pasting the Uniswap code into the same file and selecting it all. Highlighting or selecting chunks of code will usually also highlight other exact matches of your selection elsewhere in the code. But you get to go line by line, so I’ll wait here.
Now that we have confirmed our interfaces are correct, we can move to the seventh result and make sure it’s implemented correctly. Our contract accesses the interface functions only once on line 1481, it is accessed as part of our “createLiquidityCrisis” function that spans lines 1465 to 1494.
We are looking to confirm two things: who can call this function and when can it be called. You can see on lines 1470 and 1471 the lines that start with “require”. This means the code in the brackets after the word require must return as “true,” or the function will not work. If the statement requirements are not met, the phrase in quotations will be returned to the function caller as the error message. So we need to dissect these two lines and check everything they reference.
Line 1470 specifies when the function can be called, and 1471 specifies who can call it. Who can call it is more straightforward so let’s start there. Note the || in the middle of the line. This means that the require statement will return true and execute if the code on either side of the || is true at the time of calling. On the left we see “msg.sender == owner” so we need to search for owner in the contract and see who that is. On line 1289 the owner address is created, and on line 1316 the owner address is set to msg.sender inside the constructor. A constructor is the initial code that runs when a contract is created, this means whatever address deployed the contract will be its “owner”. From this we learned that only the address that deployed the contract can call this function or it will fail.
Its important to note that the word “owner” could be any word, and this does not give us any ability to change the contract. Line 1325 is a modifier named onlyOwner, any function with this modifier can only be called by the address with the “owner” label. I encourage you to search the contract for when this modifier is used to restrict other functions, to confirm that nothing is restricted to the team that shouldn’t be, but that’s outside the scope of this already long article.
The other half of the require statement on 1471 is a bit of a troll. It specifies that anyone can call the function by paying 100 ETH, but the removed liquidity still goes to devs and removed tokens are still added to the staking pool. Basically, this gives whales the option to pay us a bunch of money to pull 25% of the rug.
This does not affect the time limit of ten or more months. But when there is only 25% of the liquidity left in more than ten months (three months after the third call of the createLiquidityCrisis function) this will act an extra warning that the liquidity can go at any time. Once the timer is up you shouldn’t trust us to not call the function, because there is no way to verify our honesty. As always, the most important thing to remember is: don’t trust, verify.
So now we know who can call the function, so let’s see when it can be called. The code states “block.timestamp >= minLiquidityCrisisTime” which means the timestamp of the block that includes the transaction which calls the function must be greater than or equal to “minLiquidityCrisisTime”. This is a number, so let’s figure out what number it is.
On line 1306 we see “uint256 minLiquidityCrisisTime;”. uint256 quite literally means number, and the rest is the name we gave to that number. For a function to access or change this number, it just uses the name, in this case the number is named “minLiquidityCrisisTime”.
We first assign a value to this number on line 1418 as part of the “start” function. The start can only be called once as specified by its require statements, therefore when we set the number here, it can never be set to a different number by this method. We use this code to set the number “minLiquidityCrisisTime = block.timestamp + 60 * 60 * 24 * 30”. This takes the timestamp of the current block and adds bunch of seconds to it. The first 60 is seconds, which is what timestamp is measured in, so one minute. Multiplied by 60 brings it to one hour, then by 24 brings it to one day, and finally we multiply by 30 to give us one month. Put more simply, the number named “minLiquidityCrisisTime” is set to one month after the start function is called.
Next for a very brief overview of the rest of the “createLiquidityCrisis” function. The most important part, when it can be called, has been covered. The first thing that happens after the required criteria is met, is resetting the timer on when it can be called again. The math is the same as above, except now its set to three months from the current timestamp because the last number is 90 instead of 30. So every time this is called we have to wait three months for it to be called again. Waiting longer to call it extends the overall time the lock will be active for, which is how we created a dynamic timer without giving ourselves control of anything we shouldn’t have in a decentralized contract.
The next four lines (starting at 1475) is where we set the number of tokens to be removed that will be redeemed. We create a uint (a number) named liquidity and set it to the number “initialLiquidityTokens” and divide it by four. “initialLiquidityTokens” is created blank at the top of the contract, then set as part of the start function when the liquidity was first locked after the sale, it only appears these three times in our contract. The next three of these four lines are a safety measure to make sure we can remove the tokens if the numbers don’t exactly match on the last call.
The next eight lines starting at 1480 is where we redeem the pool tokens, so pay close attention. First we grant approval, and we only grant approval for the amount we just set and named “liquidity”. Next we call “removeLiquidityETH” and only redeem that same amount. So on both lines 1480 and 1483 exists a check to make sure we are only moving 25% of the initial tokens we put in. This could be considered, dare I say it, a double liquidity lock. Finally, the bottom three lines (starting at 1489) transfer the removed ETH to the address that launched the contract, then transfer the removed UPP tokens to the staking contract to increase the rewards pool.
Checking Our Contract For Extra Mint Abilities
Searching the word “mint” will return 13 results. The last five of those fall within the contract we wrote, and everything above is part of the imported interfaces from Uniswap, the ERC20 standard, or just used in comments.
We’re starting at the bottom and we’re looking at the two functions that start on lines 1423 and 1434. “receive” is a type of “fallback” function, which means when someone sends ETH to the contract without any data about what it wants the contract to do, this logic executes. This is how we enabled people to send ETH directly to the contract during the sale phase and receive tokens in return. The “buy” functions work pretty much the same way and perform the same checks.
These are the two functions that mint a set amount of tokens per ETH sent to the contract during the sale. The identical require statements inside both these functions are right above the mint line. So before tokens are minted it checks the group manager contract is not set, meaning the sale phase is still on, and it checks that the max tokens set in the sale phase has not been reached.
The other three instances of mint we need to look at are all within the “start” function that we’ve mentioned a few times and gone over how it can only be called once. The three lines starting at line 1393 each mint a set number of tokens. These are percentage based, and a different amount would have been minted if a different amount was minted in the sale.
So we have five instances of mint being called: two are during the sale phase, and the next three are part of the one-time function that activates the system after the sale is over. Your homework is to trace back everything related to the require statements, and make sure there is no way for us to alter the contract state to make them return true after the activation.
I hope you made it this far, or even better that you learned something new. Trust is the most valuable thing known to humans, it can take a lifetime to earn, only seconds to lose, and its often impossible to get back. So don’t trust some Twitter influencer, or your degen TG shitposter bro. Learn how this stuff works, learn what to look for, learn how to spot the tricks, learn how to stay safe and secure your bag for the moon mission. Never forget the most important phrase in crypto: don’t trust, verify.