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

The funds of `AirdropVault` can be utilized by `Staking or anybody` as the manager, and vice versa. Potentially resulting in designated vaults running out of funds.

Summary

The issue is present in LoveToken.sol. Either AirdropVault or StakingVault can re-init the LoveToken::initVault function to mint LoveTokens and approve associated Vault Managers , Which is also expected by the Soulmate Protocol. However, there anyone can be the Vault Manager and it's terrifying. There could have mistakenly wrong Managers addresses. There's no sanitization for Manager Contracts.

// ---------------------------------
// ---------------- ||
// -------- \/
@> function initVault(address managerContract) public {
if (msg.sender == airdropVault) {
_mint(airdropVault, 500_000_000 ether);
approve(managerContract, 500_000_000 ether);
emit AirdropInitialized(managerContract);
} else if (msg.sender == stakingVault) {
_mint(stakingVault, 500_000_000 ether);
approve(managerContract, 500_000_000 ether);
emit StakingInitialized(managerContract);
} else revert LoveToken__Unauthorized();
}

Vulnerability Details

Unknown Vault Manager:
  1. Place the following test code snippet into the test/unit/soulmateTest.t.sol file. Put it at the very bottom but before the last closing semicolon }.

function test_unknownVaultManager() public {
Vault airdropVault_tst = new Vault();
Vault stakingVault_tst = new Vault();
Soulmate soulmateContract_tst = new Soulmate();
LoveToken loveToken_tst = new LoveToken(
ISoulmate(address(soulmateContract_tst)), address(airdropVault_tst), address(stakingVault_tst)
);
Airdrop airdropContract_tst = new Airdrop(
ILoveToken(address(loveToken_tst)),
ISoulmate(address(soulmateContract_tst)),
IVault(address(airdropVault_tst))
);
Staking stakingContract_tst = new Staking(
ILoveToken(address(loveToken_tst)),
ISoulmate(address(soulmateContract_tst)),
IVault(address(stakingVault_tst))
);
address voldemort = makeAddr("VOLDEMORT");
airdropVault_tst.initVault(ILoveToken(address(loveToken_tst)), voldemort);
vm.startPrank(address(airdropVault_tst));
ILoveToken(address(loveToken_tst)).initVault(address(stakingContract_tst));
vm.stopPrank();
uint256 totalSupply = loveToken_tst.totalSupply();
uint256 airdropVaultBalance = loveToken_tst.balanceOf(address(airdropVault_tst));
uint256 airdropAsManagerSpendAllowance =
loveToken_tst.allowance(address(airdropVault_tst), address(airdropContract_tst));
uint256 stakingVaultBalance = loveToken_tst.balanceOf(address(stakingVault_tst));
uint256 stakingAsManagerSpendAllowance =
loveToken_tst.allowance(address(stakingVault_tst), address(stakingContract_tst));
uint256 stakingAsManagerSpendAllowanceFromAirdropVault =
loveToken_tst.allowance(address(airdropVault_tst), address(stakingContract_tst));
uint256 voldemortAsManagerSpendAllowanceFromAirdropVault =
loveToken_tst.allowance(address(airdropVault_tst), address(voldemort));
console2.log("total supply : ", totalSupply);
console2.log("airdropVaultBalance : ", airdropVaultBalance);
console2.log("airdropAsManagerSpendAllowance : ", airdropAsManagerSpendAllowance);
console2.log("stakingVaultBalance : ", stakingVaultBalance);
console2.log("stakingAsManagerSpendAllowance : ", stakingAsManagerSpendAllowance);
console2.log(
"stakingAsManagerSpendAllowanceFromAirdropVault : ", stakingAsManagerSpendAllowanceFromAirdropVault
);
console2.log(
"voldemortAsManagerSpendAllowanceFromAirdropVault : ", voldemortAsManagerSpendAllowanceFromAirdropVault
);
console2.log("Now Staking Contract can spend the funds of Airdrop Vault and vice versa!");
console2.log(
"We can also see that `voldemort` is now the manager who can simply steal the `AirdropVault` allowed funds!"
);
console2.log("------------------------------------------------------------------------------");
address voldemortAnotherAccount = makeAddr("VOLDEMORT_2");
uint256 voldemortAnotherAccountLoveTokenBalanceBeforeTransfer =
ILoveToken(address(loveToken_tst)).balanceOf(voldemortAnotherAccount);
console2.log("Voldemort EOA/Contract address : ", voldemort);
console2.log("Voldemort another EOA/Contract address : ", voldemortAnotherAccount);
console2.log(
"voldemortAnotherAccountLoveTokenBalanceBeforeTransfer : ",
voldemortAnotherAccountLoveTokenBalanceBeforeTransfer
);
vm.startPrank(voldemort);
ILoveToken(address(loveToken_tst)).transferFrom(
address(airdropVault_tst), voldemortAnotherAccount, voldemortAsManagerSpendAllowanceFromAirdropVault
);
vm.stopPrank();
uint256 airdropVaultBalanceUpdated = loveToken_tst.balanceOf(address(airdropVault_tst));
uint256 voldemortAnotherAccountLoveTokenBalanceAfterTransfer =
ILoveToken(address(loveToken_tst)).balanceOf(voldemortAnotherAccount);
console2.log(
"voldemortAnotherAccountLoveTokenBalanceAfterTransfer : ",
voldemortAnotherAccountLoveTokenBalanceAfterTransfer
);
console2.log("airdropVaultBalanceUpdated: ", airdropVaultBalanceUpdated);
console2.log("Now, Voldemort could elope with the stolen LoveTokens or even stake them to double their amount.");
}
  1. Open Your Bash Terminal and execute the following command...

forge test --mt "test_unknownVaultManager" -vvv --via-ir
  1. Please read the Output carefully that might be printed onto the terminal. Output should indicate that test Passed Successfully and the voldemort has misused his managerial power therefore stolen all the allowed funds.

Impact

If anyone discovers that they're the manager of either of the Vaults, they could misuse their managerial power, leading to significant loss of funds. There are several potential scenarios:

  1. If the Staking Contract becomes the manager, the Protocol would struggle to reward soulmates with 1 Love Token, which could result in an insider issue. Conversely, if the Airdrop Contract becomes the manager, similar difficulties may arise.

  2. If an anonymous individual or Contract or even a Bot, becomes the manager by mistake, the Protocol could lose all of its Vault-associated allowed funds, which would be a terrifying prospect.

The likelihood of this happening is very high, and here's why: a MEV BOT could potentially FrontRun the Protocol either during initialization or at a later stage. The severity of this issue is also high because all the allowed Funds of the Vault(s) Pool could run out of funds as a result.

Tools Used

Foundry Framework (Solidity, Rust)

Recommendations

Suggested Mitigation is...

  1. Static Mitigation: Create & Save All two managers on LoveToken Contract's Constructor/Initialization and allow Vaults to mint & approve LoveTokens if and only if EITHER the msg.sender == airdropVault OR msg.sender == stakingVault and then Pass the associated managers to approve LoveToken spent.

Mitigation code should be similar to the following below code....

Update files as like give below...

  1. src/LoveToken.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;
import {ERC20} from "@solmate/tokens/ERC20.sol";
import {ISoulmate} from "./interface/ISoulmate.sol";
+ import {ILoveToken} from "./interface/ILoveToken.sol";
+ import {IVault} from "./interface/IVault.sol";
+ import {Airdrop} from "./Airdrop.sol";
+ import {Staking} from "./Staking.sol";
/// @title LoveToken ERC20.
/// @author n0kto
/// @notice An ERC20 Token who are airdrop to Soulmate NFT holders.
contract LoveToken is ERC20 {
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
error LoveToken__Unauthorized();
/*//////////////////////////////////////////////////////////////
STATE VARIABLES
//////////////////////////////////////////////////////////////*/
ISoulmate public immutable soulmateContract;
address public immutable airdropVault;
address public immutable stakingVault;
+ address public immutable airdropContract;
+ address public immutable stakingContract;
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event AirdropInitialized(address indexed airdropContract);
event StakingInitialized(address indexed stakingContract);
/*//////////////////////////////////////////////////////////////
FUNCTIONS
//////////////////////////////////////////////////////////////*/
constructor(ISoulmate _soulmateContract, address _airdropVault, address _stakingVault)
ERC20("LoveToken", "<3", 18)
{
soulmateContract = _soulmateContract;
airdropVault = _airdropVault;
stakingVault = _stakingVault;
+ airdropContract = address(new Airdrop(ILoveToken(address(this)), _soulmateContract, IVault(_airdropVault)));
+ stakingContract = address(new Staking(ILoveToken(address(this)), _soulmateContract, IVault(_stakingVault)));
}
/// @notice Called at the launch of the protocol.
/// @notice Will distribute all the supply to Airdrop and Staking Contract.
- function initVault(address managerContract) public {
function initVault() public {
if (msg.sender == airdropVault) {
_mint(airdropVault, 500_000_000 ether);
- approve(managerContract, 500_000_000 ether);
+ approve(airdropContract, 500_000_000 ether);
- emit AirdropInitialized(managerContract);
+ emit AirdropInitialized(airdropContract);
} else if (msg.sender == stakingVault) {
_mint(stakingVault, 500_000_000 ether);
- approve(managerContract, 500_000_000 ether);
+ approve(stakingContract, 500_000_000 ether);
- emit StakingInitialized(managerContract);
+ emit StakingInitialized(stakingContract);
} else {
revert LoveToken__Unauthorized();
}
}
}
  1. src/Vault.sol

- function initVault(ILoveToken loveToken, address managerContract) public {
+ function initVault(ILoveToken loveToken) public {
if (vaultInitialize) revert Vault__AlreadyInitialized();
- loveToken.initVault(managerContract);
+ loveToken.initVault();
vaultInitialize = true;
}
  1. src/interface/ILoveToken.sol

interface ILoveToken {
function decimals() external returns (uint8);
function approve(address to, uint256 amount) external;
function transfer(address to, uint256 amount) external;
function transferFrom(address from, address to, uint256 amount) external;
function balanceOf(address user) external returns (uint256 balance);
function claim() external;
- function initVault(address manager) external;
+ function initVault() external;
}
  1. Dynamic Mitigation: Create & Save a dynamic array of managers on LoveToken Contract's Constructor/Initialization and allow Vaults to mint & approve LoveTokens if and only if EITHER the msg.sender == airdropVault OR msg.sender == stakingVault and then Pass the associated managers to approve LoveToken spent.

Mitigation code should be similar to the following below code....

Update src/LoveToken.sol as like give below...

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;
import {ERC20} from "@solmate/tokens/ERC20.sol";
import {ISoulmate} from "./interface/ISoulmate.sol";
+ import {ILoveToken} from "./interface/ILoveToken.sol";
+ import {IVault} from "./interface/IVault.sol";
+ import {Airdrop} from "./Airdrop.sol";
+ import {Staking} from "./Staking.sol";
/// @title LoveToken ERC20.
/// @author n0kto
/// @notice An ERC20 Token who are airdrop to Soulmate NFT holders.
contract LoveToken is ERC20 {
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
error LoveToken__Unauthorized();
/*//////////////////////////////////////////////////////////////
STATE VARIABLES
//////////////////////////////////////////////////////////////*/
ISoulmate public immutable soulmateContract;
address public immutable airdropVault;
address public immutable stakingVault;
+ address[] public airdropManagers;
+ address[] public stakingManagers;
+ mapping(address manager => bool isValid) private s_valiAirdropdManager;
+ mapping(address manager => bool isValid) private s_valiStakingdManager;
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event AirdropInitialized(address indexed airdropContract);
event StakingInitialized(address indexed stakingContract);
/*//////////////////////////////////////////////////////////////
FUNCTIONS
//////////////////////////////////////////////////////////////*/
constructor(
ISoulmate _soulmateContract,
address _airdropVault,
- address _stakingVault
+ address _stakingVault,
+ uint256 _numberOfAirdropContracts,
+ uint256 _numberOfStakingContract,
+ address[] memory _externalAirdropManagers,
+ address[] memory _externalStakingManagers
) ERC20("LoveToken", "<3", 18) {
soulmateContract = _soulmateContract;
airdropVault = _airdropVault;
stakingVault = _stakingVault;
+ for (uint256 i = 0; i <= _numberOfAirdropContracts; i++) {
+ address airdropContract =
+ address(new Airdrop(ILoveToken(address(this)), _soulmateContract, IVault(_airdropVault)));
+ s_valiAirdropdManager[airdropContract] = true;
+ airdropManagers.push(airdropContract);
+ }
+ for (uint256 j = 0; j <= _numberOfStakingContract; j++) {
+ address stakingContract =
+ address(new Staking(ILoveToken(address(this)), _soulmateContract, IVault(_stakingVault)));
+ s_valiStakingdManager[stakingContract] = true;
+ stakingManagers.push(stakingContract);
+ }
+ for (uint256 k = 0; k < _externalAirdropManagers.length; k++) {
+ s_valiAirdropdManager[_externalAirdropManagers[k]] = true;
+ airdropManagers.push(_externalAirdropManagers[k]);
+ }
+ for (uint256 f = 0; f < _externalStakingManagers.length; f++) {
+ s_valiStakingdManager[_externalStakingManagers[f]] = true;
+ stakingManagers.push(_externalStakingManagers[f]);
+ }
+ }
/// @notice Called at the launch of the protocol.
/// @notice Will distribute all the supply to Airdrop and Staking Contract.
function initVault(address managerContract) public {
- if (msg.sender == airdropVault) {
+ if (msg.sender == airdropVault && s_valiAirdropdManager[managerContract]) {
_mint(airdropVault, 500_000_000 ether);
approve(managerContract, 500_000_000 ether);
emit AirdropInitialized(managerContract);
- } else if (msg.sender == stakingVault) {
+ } else if (msg.sender == stakingVault && s_valiStakingdManager[managerContract]) {
_mint(stakingVault, 500_000_000 ether);
approve(managerContract, 500_000_000 ether);
emit StakingInitialized(managerContract);
} else revert LoveToken__Unauthorized();
}
+ function getAirdropManagersLength() external view returns (uint256) {
+ return airdropManagers.length;
+ }
+ function getStakingManagersLength() external view returns (uint256) {
+ return stakingManagers.length;
+ }
}
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.