Submitted by Luc1jan, also found by hgrano and t0x1c
https://github.com/code-423n4/2025-03-nudgexyz/blob/382a59c315b8a421f2acae5fd856bb9ca48a7a10/src/campaign/NudgeCampaign.sol#L164-L233
To participate in campaign, user has to call swap provider (Li.Fi) Executor::swapAndExecute function. This function performs swap required to get campaign target tokens and calls NudgeCampaign::handleReallocation. NudgeCampaign will receive and forward target tokens to userAddress and update user participation details accordingly. handleReallocation is only callable by SWAP_CALLER_ROLE which is given to Executor to make sure user swaps tokens before being able to participate.
However, user can encode NudgeCampaign::handleReallocation calldata inside Executor::swapAndExecute and set arbitrary userAddress that doesnt have to be the same as the swap initiator address. This allows user to gift allocation to anyone. Protocol backend will track userAddress target tokens balance and invalidate participation if the balance drops below participation amount, so this should not be an issue.
Yet, this becomes problematic if there is an existing Uniswap V2 pool in which one of the tokens is target token. This is highly probable, since target tokens must have a DEX pool in order to be bought via swap provider. Malicious user could take advantage of this and create smart contract that would initiate a flash swap from the pool, borrow substantial amount of target tokens, and encode a swap call on Executor::swapAndExecute such that it forwards all tokens to the NudgeCampaign calling handleReallocation with userAddress of the Uniswap pool. NudgeCampaign would register valid participation because handleReallocation was called from Executor and monitoring system wouldnt invalidate participation since pool has more than enough tokens to pass the balance checks. Flash swap would be successful because NudgeCampaign would send all tokens back to pool in the same transaction.
Attacker would have to buy some tokens to cover Uniswaps 0.3% fee. This is the only cost for the attacker. Fee can be calculated using unallocated amount and PPQ (rewards factor):
Its important to note here that funds can be recovered if protocol invalidates malicious participation manually. If this doesnt happen, funds will stay locked in NudgeCampaign. More importantly, other users wont be able to participate so whole campaign would be in Denial of Service and with good chance that nobody would even notice it, most likely being marked as success, while campaign admin basically burned rewards for no buying volume in return which is the reason campaign is created for.
Additionally, the exploit wouldnt work with any other lending protocol because of different flash loan implementation details. Here, for flash loan to be successful pool will verify that funds are returned by checking pool balance at the end, while other protocols usually pull the borrowed funds from the borrower which in this case wouldnt be possible since NudgeCampaign returns funds for attacker who is borrower.
Install Uniswap v2 and Li.Fi repositories (note that we are not using official Uniswap repo because of version mismatch to simplify PoC, it runs successfully on official v2-core contracts, but they require some editing to be able to compile with Solidity 0.8):
Update remappings.txt:
Flatten the UniswapV2Factory.sol:
Fix the version in lib/uniswapv2-solc0.8/contracts/UniswapV2Factory.flattened.sol:
Edit line 166 in lib/contracts/src/Periphery/Executor.sol to simplify PoC:
Create Attack.sol contract in src/mocks/:
Update test/NudgeCampaign.t.sol:
To run:
Test demonstrates that attacker can basically route borrowed tokens from flash swap through Executor and NudgeCampaign, register new participation as Uniswap V2 Pool and return these tokens, all in the same transaction. Effectively, allocating all rewards for no value provided to campaign owners and making campaign unusable, while protocol would treat it as success.
You could modify the NudgeCampaign::handleReallocation such that transaction initiator must be the actual participator:
Or, you could blacklist pools addresses and let monitoring system do the invalidation.
raphael (Nudge.xyz) confirmed
For this audit, 15 reports were submitted by wardens detailing low risk and non-critical issues. The report highlighted below by calc1f4r received the top score from the judge.
The following wardens also submitted reports: 0xshuayb, 0xWeakSheep, bigbear1229, BRONZEDISC, BUGBeast15, dd0x7e8, Ekene, holtzzx, Jatique, kazan, mitrev, rama_tavanam, teoslaf, and uba081.
