On January 15, 2023 at 05:43:37 PM UTC, according to Numen’s on-chain monitoring, the Jarvis_Network project was attacked and 663101 MATICs were lost as a result. The specific transaction can be viewed at https://polygonscan.com/tx/0x0053490215baf541362fc78be0de98e3147f40223238d5b12512b3e26c0a2c2f.
According to the analysis of the call stack, the transaction involved many token transfers, resulting in a long call stack. Our analysis revealed that there was a reentrant logic in the call process. The reentry process showed that the same function call of the same contract had the same incoming parameters, but the return value was very different before and after reentry.
The re-entrancy occurred in the remove_liquidity function of the contract at https://polygonscan.com/address/0xfb6fe7802ba9290ef8b00ca16af4bc26eb663a28. The remove_liquidity function returns tokens to the user when removing liquidity. Since Polygon and EVM are isomorphic chains, it enters the contract reentry when MATIC transfers tokens to the contract. A detailed analysis of the reentrant part of the call stack was done.
The first contract analyzed was getUnderlyingPrice at https://polygonscan.com/address/0xcc6aa628516bb46391b05b16e5058c877461cc76, which is a logical contract but not open-source.
According to the slot, we obtained v1 = 0xe7cea2f6d7b120174bf3a9bc98efaf1ff72c997d and wtoken = 0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270. We then entered the if branch and found _oracles[v1] = 0xacf3e1c6f2d6ff12b8aee44413d6834774b3f7a3 through the slot. We continued by entering the else branch inside and calling the getUnderlyingPrice function of the 0xacf3e1c6f2d6ff12b8aee44413d6834774b3f7a3 contract. The logical contract corresponding to 0xacf3e1c6f2d6ff12b8aee44413d6834774b3f7a3 is 0x3803527dcd92ac3e72a0a164db82734daba47fac, and this contract is not open source either.
The attack involves a built-in calculation because it involves re-entry, and some variables must be calculated after the transfer occurs. This part of the code does not involve external variables, so the problem likely does not lie here. Among other things, the function 0x9de is called.
The variable varg0 is the incoming token address, which is 0xe7cea2f6d7b120174bf3a9bc98efaf1ff72c997d. The corresponding _poolOf[v0] is 0xfb6fe7802ba9290ef8b00ca16af4bc26eb663a28. The get_virtual_price function of this contract is then analyzed. The code for this function is as follows:
The variable self.token is still 0xe7cea2f6d7b120174bf3a9bc98efaf1ff72c997d, and the divisor is the total circulation of this token.
It is observed that the variable D affects the return value of get_xcp. The reentrancy occurs in the remove_liquidity function, which is then analyzed. The detailed code for this function is as follows:
Changes in the return value of get_virtual_price before and after the call are noted.
The change of self.D occurs after the transfer. When the attacker removes liquidity, the MATIC is transferred to the attacker’s contract. When the fallback is called back, the price of 0xe7cea2f6d7b120174bf3a9bc98efaf1ff72c997d is checked first. Since self.D is updated after the transfer, the previous price is obtained incorrectly.
The process of removing liquidity is as follows: 1) destroying the user’s LP, 2) sending the user’s collateral funds, and 3) updating the self.D variable.
Self.D is used in the calculation of prices and is also updated when adding liquidity. However, in this instance the attacker had a larger amount of liquidity funds. According to the formula for calculating self.D, which is self.D = D – D * amount / total_supply, the value of self.D will be smaller when calculated under normal circumstances as the amount and total_supply are nearly equal.
However, since the attacker made a re-entry and increased the borrowing by a factor of 10 above the original price, the value of self.D increased when liquidity was added and was not updated in time when it was removed.
The remove_liquidity method includes a non-reentrant lock (‘lock’) to prevent the method from being reentered, but the lock does not work as the attacker reenters into another contract to borrow funds.
The attack was caused primarily by the logic of modifying variables being after the external call, resulting in an abnormal acquisition of prices. NUMEN Lab reminds project parties that their code should be subject to strict security audits. Modifying variables should occur before external calls, and prices should be obtained from multiple data sources. This approach will make the project more secure and stable.
Should you wish to audit and ensure that your projects are free from exploits such as these, reach out to us here.
Numen Cyber Labs is committed to facilitating the safe development of Web 3.0. We are dedicated to the security of the blockchain ecosystem, as well as operating systems & browser/mobile security. We regularly disseminate analyses on topics such as these, please stay tuned or visit our blog here for more!