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)) {
super._update(from, to, amount);
return;
}
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.
-
Step 1: Create a Foundry project and place all the contracts in the src
directory.
-
Step 2: Create a test
directory and a mocks
folder within the src
directory (or use an existing mocks folder).
-
Step 3: Create all necessary mock contracts, if required.
-
Step 4: Create a test file (with any name) in the test
directory.
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;
uint256 initialRaacBurnTaxRateInBps = 150;
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();
}
}
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();
vm.startPrank(RAAC_MINTER);
raacToken.mint(DEVIL, LOCK_AMOUNT);
vm.stopPrank();
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));
vm.startPrank(DEVIL);
veRaacToken.approve(ALICE, veRaacToken.balanceOf(DEVIL));
vm.stopPrank();
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();
}
Step 6: To run the test, execute the following commands in your terminal:
forge test --mt testVeTokensCanBeUsedToPhishPublic -vv
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
Devil
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
Setup:
Approval:
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.
Outcome:
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