Report in progress

Upside

A social prediction market. Imagine PumpFun had a baby with Reddit. That's Upside.

  • Start date19 May 2025
  • End date26 May 2025
  • Total awards$13,000 in USDC
  • Duration7 days

Upside audit details

  • Total Prize Pool: $13,000 in USDC
    • HM awards: up to $9,600 in USDC
      • If no valid Highs or Mediums are found, the HM pool is $0
    • QA awards: $400 in USDC
    • Judge awards: $2,500 in USDC
    • Scout awards: $500 in USDC
  • Read our guidelines for more details
  • Starts May 19, 2025 20:00 UTC
  • Ends May 26, 2025 20:00 UTC

❗️ Important notes for wardens

  1. A coded, runnable PoC is required for all High/Medium submissions to this audit.
    • This audit repo includes a basic template to run the test suite.
    • PoCs must use the test suite provided in the audit repo.
    • Your submission will be marked as Insufficient if the POC is not runnable and working with the provided test suite.
    • Exception: PoC is optional (though recommended) for wardens with signal ≥ 0.68
  2. Judging phase risk adjustments (upgrades/downgrades):
    • High- or Medium-risk submissions downgraded to Low-risk (QA) will be ineligible for awards.
    • Upgrading a Low-risk finding from a QA report to a Medium- or High-risk finding is not supported.
    • As such, wardens are encouraged to select the appropriate risk level carefully during the submission phase.

Automated Findings / Publicly Known Issues

The 4naly3er report can be found here.

Note for C4 wardens: Anything included in this Automated Findings / Publicly Known Issues section is considered a publicly known issue and is ineligible for awards.

🔒 Global Liquidity Withdrawal Timer

  • The withdrawLiquidity function uses a global cooldown timer.
  • Once this timer is triggered and expires, the Owner can withdraw liquidity from all MetaCoins, both past and future.
  • This behavior is by design and not scoped per MetaCoin.

💵 Liquidity Token is Assumed to be USDC

  • INITIAL_LIQUIDITY_RESERVES is hardcoded to 10_000 * 10^6, assuming USDC with 6 decimals.

🔁 Swap Function Reentrancy Consideration

  • The swap() function makes external token transfers (e.g., transfer, approve).
  • Reentrancy guards are intentionally omitted, relying instead on the assumption that only trusted tokens (USDC, MetaCoins) are used.
  • Given this closed token set, reentrancy is not considered a practical risk.

🧾 Zero-Value Claims & Transfers

  • Both the protocol and deployer can call claim* functions even if their claimable balance is 0.

🪪 Token Name Changes & Permit Compatibility

  • Owners can call setNameAndSymbol() to update the name/symbol of a MetaCoin.
  • This may break ERC20 Permit compatibility due to EIP-712 domain separation relying on the name() value.
  • Integrators using permit() should handle potential mismatches or restrict tokens that have changed their name.

Centralization Risk

It's possible for the Owner to withdraw liquidity for all tokens provided 14 days have passed. This is a global countdown timer that once passed, allows the Owner to remove liquidity for migration. This applies for ALL tokens including those that may be deployed in the future.

Supported Liquidity Tokens

The liquidity token is always intended to be USDC and any misbehaviour arising from other tokens as liquidity tokens is out-of-scope.

Previous Audits

Any vulnerability that was identified in the referenced security audit and was acknowledged should be considered out-of-scope for the purposes of this contest.

Overview

Upside is a social prediction market (think Polymarket + reddit) where you win money by being early on viral tweets, Spotify songs, YouTube videos, etc.

For a more detailed and in-depth technical overview of the project, please visit the referenced code walk-through below.


Scope

Files in scope

FileSLOCCode Coverage (Statements / Branches)PurposeLibraries used
contracts/UpsideMetaCoin.sol62100% (80%)An ERC-20 implementation whose transfers are restricted via a whitelist@openzeppelin/contracts/token/ERC20/ERC20.sol, @openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol, @openzeppelin/contracts/access/Ownable.sol
contracts/UpsideProtocol.sol317100% (98.65%)Permits the tokenization of any URL through the above token and facilitates swap and fee mechanisms on top of it@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol, @openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol, @openzeppelin/contracts/access/Ownable.sol, contracts/UpsideMetaCoin.sol
Totals379100% (89.325%)

See scope.txt for a machine-readable version

Files out of scope

See out_of_scope.txt for a machine-readable version

Additional context

Areas of concern (where to focus for bugs)

Any breach of the following invariants is considered an area of concern for us.

Main invariants

Token Restrictions

Tokenization of a URL can only happen once. Tokens are deployed with a fixed initial supply and reserves which cannot be mutated post deployment.

Whitelist Restrictions

MetaCoin transfers are restricted by whitelist unless explicitly disabled by the Owner. The Owner can permanently disable the whitelist for any MetaCoin.

Liquidity Reserves

Reserves for the liquidity token and MetaCoin are always updated after each swap. Sell swaps should never drop below INITIAL_LIQUIDITY_RESERVES.

Fee Flows

Swap Fee revenue is accumulated and must be claimed explicitly by the deployer or Owner. Transfers of claimable fees are never automatic.

Liquidity Withdrawal Restrictions

Liquidity withdrawal is restricted by a global cooldown mechanism. Once triggered, the Owner must wait 14 days before a withdrawal can be completed.

All trusted roles in the protocol

RoleDescription
Owner- Sets fee parameters; Sets staking contract address; Claims fees; Changes name / symbol of a MetaCoin; Disables whitelist; Withdraws liquidity; Sets tokenization fees; Whitelists addresses for transfers

Running tests

Setup

Clone the repository locally and change the working directory to it:

git clone https://github.com/code-423n4/2025-05-upside cd ./2025-05-upside

Make sure you have foundry (v1.1.0 tested), NodeJS (v20.9.0 tested), as well as yarn (v1.22.22 tested) installed.

Install dependencies:

npm i

Setup environment:

npx hardhat vars setup npx hardhat vars set MNEMONIC npx hardhat vars set INFURA_API_KEY npx hardhat vars set BASE_FORK_URL

Alternatively, modify the hardhat configuration file to utilize a chain configuration for one of the publicly available RPC URLs such as Avalanche or Binance Chain. Here's an example configuration file:

import "@nomicfoundation/hardhat-toolbox"; import "hardhat-deploy"; import type { HardhatUserConfig } from "hardhat/config"; import { vars } from "hardhat/config"; import type { NetworkUserConfig } from "hardhat/types"; import "./tasks/accounts"; import "./deploy/deploy"; // Run 'npx hardhat vars setup' to see the list of variables that need to be set const mnemonic: string = "test test test test test test test test test test test junk"; const chainIds = { "arbitrum-mainnet": 42161, avalanche: 43114, bsc: 56, ganache: 1337, hardhat: 31337, mainnet: 1, "optimism-mainnet": 10, "polygon-mainnet": 137, "polygon-mumbai": 80001, sepolia: 11155111, base: 8453, }; function getChainConfig(chain: keyof typeof chainIds): NetworkUserConfig { let jsonRpcUrl: string; switch (chain) { case "avalanche": jsonRpcUrl = "https://api.avax.network/ext/bc/C/rpc"; break; default: jsonRpcUrl = "https://bsc-dataseed1.binance.org"; } return { accounts: { count: 10, mnemonic, path: "m/44'/60'/0'/0", }, chainId: chainIds[chain], url: jsonRpcUrl, }; } const config: HardhatUserConfig = { defaultNetwork: "hardhat", namedAccounts: { deployer: 0, }, etherscan: { apiKey: { arbitrumOne: vars.get("ARBISCAN_API_KEY", ""), avalanche: vars.get("SNOWTRACE_API_KEY", ""), bsc: vars.get("BSCSCAN_API_KEY", ""), mainnet: vars.get("ETHERSCAN_API_KEY", ""), optimisticEthereum: vars.get("OPTIMISM_API_KEY", ""), polygon: vars.get("POLYGONSCAN_API_KEY", ""), polygonMumbai: vars.get("POLYGONSCAN_API_KEY", ""), sepolia: vars.get("ETHERSCAN_API_KEY", ""), base: vars.get("BASESCAN_API_KEY", ""), }, }, gasReporter: { currency: "USD", enabled: process.env.REPORT_GAS ? true : false, excludeContracts: [], src: "./contracts", }, networks: { hardhat: { accounts: { mnemonic, }, chainId: chainIds.hardhat, }, ganache: { accounts: { mnemonic, }, chainId: chainIds.ganache, url: "http://localhost:8545", }, arbitrum: getChainConfig("arbitrum-mainnet"), avalanche: getChainConfig("avalanche"), bsc: getChainConfig("bsc"), mainnet: getChainConfig("mainnet"), optimism: getChainConfig("optimism-mainnet"), "polygon-mainnet": getChainConfig("polygon-mainnet"), "polygon-mumbai": getChainConfig("polygon-mumbai"), sepolia: getChainConfig("sepolia"), base: getChainConfig("base"), }, paths: { artifacts: "./artifacts", cache: "./cache", sources: "./contracts", tests: "./test", }, solidity: { version: "0.8.24", settings: { metadata: { // Not including the metadata hash // https://github.com/paulrberg/hardhat-template/issues/31 bytecodeHash: "none", }, // Disable the optimizer when debugging // https://hardhat.org/hardhat-network/#solidity-optimizer-support optimizer: { enabled: true, runs: 800, }, }, }, typechain: { outDir: "types", target: "ethers-v6", }, }; export default config;

So as to be able to run the default test suite of the project, you will need to configure the Coinbase fork URL (BASE_FORK_URL) as the test files reference actual deployments of tokens such as USDC.

Please make sure that the RPC end point you're pointing to supports historical queries up to the ones defined in the relevant test suites. You can also update the block number that each test resets the chain to to a recent block that your RPC supports.

Tests

To run tests:

yarn test

Code Coverage

For code coverage the bun JavaScript runtime is required (v1.2.13 tested).

To run code coverage:

yarn coverage

Submission PoC

As this contest contains a mandatory PoC, we have included a base test file under the test sub-folder that contains a boilerplate test which wardens are instructed to fill in with the necessary code to demonstrate their vulnerabilities (if any are found).

The PoC test file in question is test/PoC.test.ts and included below for brevity:

import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { ethers as hhethers } from "hardhat"; import {IERC20Metadata, UpsideMetaCoin, UpsideProtocol, UpsideStakingStub} from "../types"; describe("C4 PoC Test Suite", function () { let signers: HardhatEthersSigner[]; let owner: HardhatEthersSigner; let user1: HardhatEthersSigner; let stakingContract: UpsideStakingStub; let upsideProtocol: UpsideProtocol; let liquidityToken: IERC20Metadata; let sampleLinkToken: UpsideMetaCoin; before(async function () { signers = await hhethers.getSigners(); owner = signers[2]; user1 = signers[0]; const upsideStakingStubFactory = await hhethers.getContractFactory("UpsideStakingStub"); stakingContract = await upsideStakingStubFactory.connect(owner).deploy(owner.address); await stakingContract.connect(owner).setFeeDestinationAddress(owner.address); const upsideProtocolFactory = await hhethers.getContractFactory("UpsideProtocol"); upsideProtocol = await upsideProtocolFactory.connect(owner).deploy(owner.address); // Deploy mock USDC const liquidityTokenFactory = await hhethers.getContractFactory("USDCMock"); liquidityToken = await liquidityTokenFactory.connect(owner).deploy(); // Setup protocol await upsideProtocol.connect(owner).init(await liquidityToken.getAddress()); await upsideProtocol.connect(owner).setStakingContractAddress(await stakingContract.getAddress()); // Deploy a simple Tokenized URL await upsideProtocol.connect(owner).tokenize("https://code4rena.com/", await liquidityToken.getAddress()); sampleLinkToken = await hhethers.getContractAt("UpsideMetaCoin", await upsideProtocol.urlToMetaCoinMap("https://code4rena.com/")); }); it("should demonstrate the flaw of the C4 submission", async function () { // Insert code here, tokens can be minted via liquidityToken.mint }); });

Miscellaneous

Employees of Upside and employees' family members are ineligible to participate in this audit.

Code4rena's rules cannot be overridden by the contents of this README. In case of doubt, please check with C4 staff.