Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: low
Invalid

Inadequate Approval Controls in veRAACToken Enable Phishing Attacks and Governance Manipulation

Summary

The veRAACToken contract is designed as a non-transferable ERC20 token representing governance voting power. To enforce non-transferability, the contract overrides the standard ERC20 transfer functions—transfer and transferFrom—and implements an internal _update function that reverts any transfer attempts. However, the approve function remains unmodified, allowing token holders to set allowances despite the tokens being non-transferable.

This discrepancy creates a serious phishing vulnerability. A malicious token holder, "Devil," can exploit the open approval mechanism to trick unsuspecting users, such as "Alice," into believing they can control or utilize Devil’s veRAACTokens. For example, Devil might offer to approve his veRAACTokens to Alice, promising that she can leverage that approval to gain governance influence or voting power. In return, Devil could demand compensation in the form of other tokens or assets. When Alice later attempts to spend or transfer the approved tokens, the transaction will fail due to the enforced transfer restrictions, leaving her defrauded. This attack not only undermines governance integrity but also disrupts the voting process and damages user trust in the protocol.

Vulnerability Details

Incomplete Override of ERC20 Functions

The veRAACToken contract effectively disables token transfers by overriding the standard ERC20 functions. The transfer and transferFrom functions are implemented as follows:

function transfer(address to, uint256 amount) public virtual override(ERC20, IveRAACToken) returns (bool) {
return super.transfer(to, amount);
}
function transferFrom(address from, address to, uint256 amount)
public
virtual
override(ERC20, IveRAACToken)
returns (bool)
{
return super.transferFrom(from, to, amount);
}

The critical internal function _update is then overridden to prevent any token transfer between non-zero addresses:

function _update(address from, address to, uint256 amount) internal virtual override {
if (from == address(0) || to == address(0)) {
// Allow minting and burning operations
super._update(from, to, amount);
return;
}
// Prevent all other transfers of veRAAC tokens
revert TransferNotAllowed();
}

This design ensures that veRAACTokens remain non-transferable and only exist for governance purposes.

Unrestricted Approval Mechanism

Despite the restrictions on transfers, the approve function is not overridden. This leaves the standard ERC20 approval functionality intact, allowing any token holder to grant an allowance to another address. While no one can later transfer tokens (since transfer operations are prohibited), this creates a dangerous scenario:

  • Phishing Opportunity: A malicious veRAACToken holder ("Devil") can offer to approve his tokens to a target user ("Alice"), falsely claiming that Alice can exercise control over those tokens or use them to participate in governance votes.

  • Deceptive Compensation: Devil may ask Alice for compensation in other tokens or assets in exchange for granting her the approval. However, when Alice tries to spend or transfer the approved tokens, her transactions will revert because the overridden _update function prevents any actual transfer.

  • Governance Impact: Since veRAACTokens confer voting power, such phishing attacks could disrupt the governance process.

Proof of Concept

To demonstrate this vulnerability, the following Proof of Concept (PoC) is provided. The PoC is written using the Foundry tool.

  1. Step 1: Create a Foundry project and place all the contracts in the src directory.

  2. Step 2: Create a test directory and a mocks folder within the src directory (or use an existing mocks folder).

  3. Step 3: Create all necessary mock contracts, if required.

  4. Step 4: Create a test file (with any name) in the test directory.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {veRAACToken} from "../src/core/tokens/veRAACToken.sol";
import {RAACToken} from "../src/core/tokens/RAACToken.sol";
import {TimeWeightedAverage} from "../src/libraries/math/TimeWeightedAverage.sol";
import {LockManager} from "../src/libraries/governance/LockManager.sol";
import {IveRAACToken} from "../src/interfaces/core/tokens/IveRAACToken.sol";
contract VeRAACTokenTest is Test {
veRAACToken veRaacToken;
RAACToken raacToken;
address RAAC_OWNER = makeAddr("RAAC_OWNER");
address RAAC_MINTER = makeAddr("RAAC_MINTER");
uint256 initialRaacSwapTaxRateInBps = 200; // 2%, 10000 - 100%
uint256 initialRaacBurnTaxRateInBps = 150; // 1.5%, 10000 - 100%
address VE_RAAC_OWNER = makeAddr("VE_RAAC_OWNER");
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
function setUp() public {
raacToken = new RAACToken(RAAC_OWNER, initialRaacSwapTaxRateInBps, initialRaacBurnTaxRateInBps);
vm.startPrank(VE_RAAC_OWNER);
veRaacToken = new veRAACToken(address(raacToken));
vm.stopPrank();
}
}
  1. Step 5: Add the following test PoC in the test file, after the setUp function.

function testVeTokensCanBeUsedToPhishPublic() public {
uint256 LOCK_AMOUNT = 10_000_000e18;
uint256 LOCK_DURATION = 365 days;
vm.startPrank(RAAC_OWNER);
raacToken.setMinter(RAAC_MINTER);
vm.stopPrank();
// Devil earns raacTokens
vm.startPrank(RAAC_MINTER);
raacToken.mint(DEVIL, LOCK_AMOUNT);
vm.stopPrank();
// Devil locks raacTokens to earn veRAACTokens
vm.startPrank(DEVIL);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
console.log("Devil's veRaac Token Balance before approval and transfer: ", veRaacToken.balanceOf(DEVIL));
// Devil Phishes Alice by giving approval to her
vm.startPrank(DEVIL);
veRaacToken.approve(ALICE, veRaacToken.balanceOf(DEVIL));
vm.stopPrank();
// Alice cross checks the Devil's balance and his grant
vm.startPrank(ALICE);
console.log("Devil's veRaac Token Balance after approval and transfer : ", veRaacToken.balanceOf(DEVIL));
console.log(
"Devil's veRaac Token allowance to ALICE : ", veRaacToken.allowance(DEVIL, ALICE)
);
vm.stopPrank();
vm.startPrank(ALICE);
veRaacToken.transferFrom(DEVIL, ALICE, veRaacToken.allowance(DEVIL, ALICE));
vm.stopPrank();
}
  1. Step 6: To run the test, execute the following commands in your terminal:

forge test --mt testVeTokensCanBeUsedToPhishPublic -vv
  1. Step 7: Review the output. The expected output should indicate that malicious token holders can phish others and make them victim.

[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/VeRAACTokenTest.t.sol:VeRAACTokenTest
[FAIL. Reason: TransferNotAllowed()] testVeTokensCanBeUsedToPhishPublic() (gas: 649777)
Logs:
Devil's veRaac Token Balance before approval and transfer: 2500000000000000000000000
Devil's veRaac Token Balance after approval and transfer : 2500000000000000000000000
Devil's veRaac Token allowance to ALICE : 2500000000000000000000000
Traces:
[4050171] VeRAACTokenTest::setUp()
├─ [1090648] → new RAACToken@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
│ ├─ emit OwnershipTransferred(previousOwner: 0x0000000000000000000000000000000000000000, newOwner: RAAC_OWNER: [0x26344e4d87099cE096c47598C7726aa1F6d5318D])
│ ├─ emit SwapTaxRateUpdated(newRate: 200)
│ ├─ emit BurnTaxRateUpdated(newRate: 150)
│ └─ ← [Return] 4648 bytes of code
├─ [0] VM::startPrank(VE_RAAC_OWNER: [0x2A7C4e11aF3c40bc9B6D2B522BE90284492CBAdA])
│ └─ ← [Return]
├─ [2852386] → new veRAACToken@0xB7CF82306b2D9d25c2EaaCDdE1d271E063ECD5A7
│ ├─ emit OwnershipTransferred(previousOwner: 0x0000000000000000000000000000000000000000, newOwner: VE_RAAC_OWNER: [0x2A7C4e11aF3c40bc9B6D2B522BE90284492CBAdA])
│ └─ ← [Return] 12907 bytes of code
├─ [0] VM::stopPrank()
│ └─ ← [Return]
└─ ← [Stop]
[649777] VeRAACTokenTest::testVeTokensCanBeUsedToPhishPublic()
├─ [0] VM::startPrank(RAAC_OWNER: [0x26344e4d87099cE096c47598C7726aa1F6d5318D])
│ └─ ← [Return]
├─ [25919] RAACToken::setMinter(RAAC_MINTER: [0x9E7e395cF94Ec2744aDc29c42288C76eaBd8Bdb4])
│ ├─ emit MinterSet(minter: RAAC_MINTER: [0x9E7e395cF94Ec2744aDc29c42288C76eaBd8Bdb4])
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::startPrank(RAAC_MINTER: [0x9E7e395cF94Ec2744aDc29c42288C76eaBd8Bdb4])
│ └─ ← [Return]
├─ [51517] RAACToken::mint(DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998], 10000000000000000000000000 [1e25])
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998], value: 10000000000000000000000000 [1e25])
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::startPrank(DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998])
│ └─ ← [Return]
├─ [24807] RAACToken::approve(veRAACToken: [0xB7CF82306b2D9d25c2EaaCDdE1d271E063ECD5A7], 10000000000000000000000000 [1e25])
│ ├─ emit Approval(owner: DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998], spender: veRAACToken: [0xB7CF82306b2D9d25c2EaaCDdE1d271E063ECD5A7], value: 10000000000000000000000000 [1e25])
│ └─ ← [Return] true
├─ [481489] veRAACToken::lock(10000000000000000000000000 [1e25], 31536000 [3.153e7])
│ ├─ [60964] RAACToken::transferFrom(DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998], veRAACToken: [0xB7CF82306b2D9d25c2EaaCDdE1d271E063ECD5A7], 10000000000000000000000000 [1e25])
│ │ ├─ emit Transfer(from: DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998], to: RAAC_OWNER: [0x26344e4d87099cE096c47598C7726aa1F6d5318D], value: 200000000000000000000000 [2e23])
│ │ ├─ emit Transfer(from: DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998], to: 0x0000000000000000000000000000000000000000, value: 150000000000000000000000 [1.5e23])
│ │ ├─ emit Transfer(from: DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998], to: veRAACToken: [0xB7CF82306b2D9d25c2EaaCDdE1d271E063ECD5A7], value: 9650000000000000000000000 [9.65e24])
│ │ └─ ← [Return] true
│ ├─ emit LockCreated(user: DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998], amount: 10000000000000000000000000 [1e25], unlockTime: 31536001 [3.153e7])
│ ├─ emit PeriodCreated(startTime: 1, duration: 604800 [6.048e5], initialValue: 0)
│ ├─ emit VotingPowerUpdated(user: DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998], oldPower: 0, newPower: 2500000000000000000000000 [2.5e24])
│ ├─ emit CheckpointCreated(user: DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998], blockNumber: 1, power: 2500000000000000000000000 [2.5e24])
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998], value: 2500000000000000000000000 [2.5e24])
│ ├─ emit LockCreated(user: DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998], amount: 10000000000000000000000000 [1e25], unlockTime: 31536001 [3.153e7])
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [643] veRAACToken::balanceOf(DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998]) [staticcall]
│ └─ ← [Return] 2500000000000000000000000 [2.5e24]
├─ [0] console::log("Devil's veRaac Token Balance before approval and transfer: ", 2500000000000000000000000 [2.5e24]) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::startPrank(DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998])
│ └─ ← [Return]
├─ [643] veRAACToken::balanceOf(DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998]) [staticcall]
│ └─ ← [Return] 2500000000000000000000000 [2.5e24]
├─ [24830] veRAACToken::approve(ALICE: [0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916], 2500000000000000000000000 [2.5e24])
│ ├─ emit Approval(owner: DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998], spender: ALICE: [0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916], value: 2500000000000000000000000 [2.5e24])
│ └─ ← [Return] true
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::startPrank(ALICE: [0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916])
│ └─ ← [Return]
├─ [643] veRAACToken::balanceOf(DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998]) [staticcall]
│ └─ ← [Return] 2500000000000000000000000 [2.5e24]
├─ [0] console::log("Devil's veRaac Token Balance after approval and transfer : ", 2500000000000000000000000 [2.5e24]) [staticcall]
│ └─ ← [Stop]
├─ [792] veRAACToken::allowance(DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998], ALICE: [0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916]) [staticcall]
│ └─ ← [Return] 2500000000000000000000000 [2.5e24]
├─ [0] console::log("Devil's veRaac Token allowance to ALICE : ", 2500000000000000000000000 [2.5e24]) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::startPrank(ALICE: [0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916])
│ └─ ← [Return]
├─ [792] veRAACToken::allowance(DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998], ALICE: [0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916]) [staticcall]
│ └─ ← [Return] 2500000000000000000000000 [2.5e24]
├─ [1591] veRAACToken::transferFrom(DEVIL: [0x3396C7fDA8a66182cf4135FBad98ECc100794998], ALICE: [0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916], 2500000000000000000000000 [2.5e24])
│ └─ ← [Revert] TransferNotAllowed()
└─ ← [Revert] TransferNotAllowed()
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 3.68ms (675.50µs CPU time)
Ran 1 test suite in 1.71s (3.68ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/VeRAACTokenTest.t.sol:VeRAACTokenTest
[FAIL. Reason: TransferNotAllowed()] testVeTokensCanBeUsedToPhishPublic() (gas: 649777)
Encountered a total of 1 failing tests, 0 tests succeeded

As demonstrated, the test confirms that the malicious token holder can Phish others like Alice and afterwards victims like alice will face a Denial of Service (DoS) due to non-transferrability of veRAAC Tokens.

Explanation

  1. Setup:

    • The veRAACToken contract is deployed, and Devil is assigned a balance of veRAACTokens.

  2. Approval:

    • Devil calls approve to allow Alice an allowance of his tokens.

  3. Phishing Attempt:

    • Alice, thinking she can transfer or use these tokens, calls transferFrom to move the tokens. However, due to the overridden _update function, the transfer reverts, demonstrating the flaw.

  4. Outcome:

    • The PoC confirms that although approval is granted, any subsequent transfer attempt fails, effectively enabling Devil to scam Alice.

Impact

  • Governance Manipulation:
    Malicious actors can record false approvals to mislead victims, potentially manipulating voting power in governance decisions.

  • Voter Denial of Service:
    Legitimate users may be prevented from participating in governance if their addresses are misrepresented as having already cast votes or as being controlled by malicious parties.

  • Erosion of User Trust:
    The inconsistency between the non-transferable nature of the tokens and the functioning approval mechanism can lead to confusion and loss of confidence in the protocol.

  • Phishing Risk:
    Malicious token holders may use the approval feature to lure victims into fraudulent schemes, thereby causing financial loss and reputational damage to the protocol.

Tools Used

  • Manual Review

  • Foundry

  • Console Log (Foundry)

Recommendations

  • Override or Restrict the approve Function:

    • Modify the approve function to either revert or restrict its usage in veRAACToken, given that transfers are prohibited. This can be achieved by overriding approve in the same manner as transfer and transferFrom, ensuring consistency in access control.

    • For example:

      function approve(address spender, uint256 amount) public virtual override returns (bool) {
      revert("Approval is disabled for veRAACTokens");
      }
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Appeal created

theirrationalone Submitter
4 months ago
inallhonesty Lead Judge
4 months ago
inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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