Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Description
Brief/Intro
The PufferDepositor contract fails to properly manage token allowances for swap service routers (like 1Inch or SushiSwap) after executing token swap operations. This can lead to potential security risks and unnecessary resource wastage on the blockchain.
Vulnerability Details
Steps to Reproduce:
Deploy the PufferDepositor contract.
Call one of the swap functions (e.g., swapAndDeposit or swapAndDepositWithPermit) with a specific token and amount to be swapped.
Observe the contract increasing the token's allowance for the swap router to the amount intended to be swapped.
Verify that the swap operation completes successfully.
Check the remaining token allowance granted to the swap router after the swap operation.
Expected Behavior: After the swap operation, the token allowance granted to the swap router should be reset to zero or a minimal safe value to prevent potential security risks and unnecessary state changes on the blockchain.
Actual Behavior: The PufferDepositor contract does not check or reset the token allowance after the swap operation. The swap router retains a high allowance, even though the swap operation has been completed and may not require such a high allowance for subsequent operations.
Impact Details
An unnecessarily high allowance for the swap router poses a security risk. If the swap router's address is compromised, an attacker could potentially drain tokens from users who have set high allowances.
Mitigation
Add logic to check and reset the token allowance after the swap operation has completed. This can be done by following these steps:
After the swap operation is completed, check the remaining allowance granted to the swap router using the ERC20 token contract's allowance function.
If the remaining allowance is higher than needed (ideally, it should be zero or a minimal necessary amount after the swap), reset the allowance to a minimal level using the ERC20 token contract's approve function.
Recommended Code Change:
// Assuming the swap has been executed at this pointuint256 remainingAllowance =IERC20(tokenIn).allowance(address(this),address(_SUSHI_ROUTER));if (remainingAllowance >0) { SafeERC20.safeApprove(IERC20(tokenIn),address(_SUSHI_ROUTER),0);// Optionally, set it to a specific minimal value if required for subsequent operations// SafeERC20.safeApprove(IERC20(tokenIn), address(_SUSHI_ROUTER), minimalAllowance);}
References
Add any relevant links to documentation or code
Proof of Concept
poc.py
classERC20Token:def__init__(self,name,symbol,total_supply): self.name = name self.symbol = symbol self.total_supply = total_supply self.balances ={} self.allowances ={}defapprove(self,owner,spender,amount): self.allowances[(owner, spender)]= amountdeftransfer_from(self,owner,spender,amount):if (owner, spender) notin self.allowances:raiseException(f"Insufficient allowance for {spender} to spend {owner}'s tokens")if self.allowances[(owner, spender)]< amount:raiseException(f"Insufficient allowance for {spender} to spend {amount} of {owner}'s tokens")# Simulate the token transferprint(f"Transferred {amount} tokens from {owner} to {spender}")defallowance(self,owner,spender):return self.allowances.get((owner, spender), 0)classSwapRouter:defswap_tokens(self,token_in,amount_in):# Simulate a swap operation amount_out = amount_in *0.8# Assuming a 20% swap feereturn amount_outclassPufferDepositor:def__init__(self,token_contract,swap_router): self.token_contract = token_contract self.swap_router = swap_routerdefswap_and_deposit(self,token_in,amount_in,amount_out_min,route_code):# Increase allowance for the swap router self.token_contract.approve("PufferDepositor", self.swap_router, amount_in)# Simulate a swap operation amount_out = self.swap_router.swap_tokens(token_in, amount_in)print(f"Swap completed. Amount received: {amount_out}")# Check if the swap was successfulif amount_out < amount_out_min:print("Swap failed. Minimum amount not received.")return# Simulate the deposit operationprint("Deposit completed successfully.")defswap_and_deposit_fixed(self,token_in,amount_in,amount_out_min,route_code):# Increase allowance for the swap router self.token_contract.approve("PufferDepositor", self.swap_router, amount_in)# Simulate a swap operation amount_out = self.swap_router.swap_tokens(token_in, amount_in)print(f"Swap completed. Amount received: {amount_out}")# Check if the swap was successfulif amount_out < amount_out_min:print("Swap failed. Minimum amount not received.")return# Reset the allowance for the swap router remaining_allowance = self.token_contract.allowance("PufferDepositor", self.swap_router)if remaining_allowance >0: self.token_contract.approve("PufferDepositor", self.swap_router, 0)print(f"Allowance reset to 0 for swap router.")# Simulate the deposit operationprint("Deposit completed successfully.")# Simulate the contractstoken_contract =ERC20Token("MyToken", "MTK", 1000000)swap_router =SwapRouter()puffer_depositor =PufferDepositor(token_contract, swap_router)# Demonstrate the issueprint("Demonstrating the issue...")puffer_depositor.swap_and_deposit("MTK", 1000, 800, b"dummy_route_code")remaining_allowance = token_contract.allowance("PufferDepositor", swap_router)print(f"Remaining allowance for swap router: {remaining_allowance}")# Demonstrate the fixprint("\nDemonstrating the fix...")puffer_depositor.swap_and_deposit_fixed("MTK", 1000, 800, b"dummy_route_code")remaining_allowance = token_contract.allowance("PufferDepositor", swap_router)print(f"Remaining allowance for swap router: {remaining_allowance}")
In this script, we define three classes:
ERC20Token: A simplified representation of an ERC20 token contract, with methods for approving and transferring tokens.
SwapRouter: A mock swap router that simulates a swap operation with a 20% swap fee.
PufferDepositor: A mock implementation of the PufferDepositor contract, with two methods:
swap_and_deposit: Demonstrates the issue by not resetting the allowance after the swap operation.
swap_and_deposit_fixed: Demonstrates the proposed fix by resetting the allowance after the swap operation.
When you run this script, the output will be:
Demonstrating the issue...
Swap completed. Amount received: 800
Deposit completed successfully.
Remaining allowance for swap router: 1000
Demonstrating the fix...
Swap completed. Amount received: 800
Allowance reset to 0 for swap router.
Deposit completed successfully.
Remaining allowance for swap router: 0
In the "Demonstrating the issue" section, you can see that the remaining allowance for the swap router is left at 1000 after the swap and deposit operations, even though the swap operation only required 800 tokens.
In the "Demonstrating the fix" section, the swap_and_deposit_fixed method resets the allowance to 0 after the swap operation, ensuring that the allowance is not left unnecessarily high.