Core Contracts

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

Existing veRAAC lock is overwritten when user locks again

Summary

Existing veRAAC lock is overwritten when user locks again.

Vulnerability Details

When a user calls lock() to deposits RAAC tokens, they are created with a lock.

veRAACToken::lock()

// Create lock position
_lockState.createLock(msg.sender, amount, duration);

And thier deposited RAAC token amount is store in _lockState.

LockManager::createLock()

state.locks[user] = Lock({
@> amount: amount,
end: end,
exists: true
});

However, when the user calls lock() again, the early deposited RAAC tokens are not refunded, and the amount would be overwritten, this causes loss of funds to the user.

Impact

Loss of funds.

POC

Please run forge test --mt testAudit_MultipleLocks.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console, stdError} from "forge-std/Test.sol";
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "../contracts/libraries/math/WadRayMath.sol";
import "../contracts/core/pools/LendingPool/LendingPool.sol";
import "../contracts/core/pools/StabilityPool/StabilityPool.sol";
import "../contracts/mocks/core/tokens/crvUSDToken.sol";
import "../contracts/core/tokens/RToken.sol";
import "../contracts/core/tokens/DebtToken.sol";
import "../contracts/core/tokens/DeToken.sol";
import "../contracts/core/tokens/RAACToken.sol";
import "../contracts/core/tokens/RAACNFT.sol";
import "../contracts/core/tokens/veRAACToken.sol";
import "../contracts/core/primitives/RAACHousePrices.sol";
import "../contracts/core/minters/RAACMinter/RAACMinter.sol";
import "../contracts/core/collectors/FeeCollector.sol";
import "../contracts/core/collectors/Treasury.sol";
contract Audit is Test {
using WadRayMath for uint256;
using SafeCast for uint256;
address owner = makeAddr("Owner");
address repairFund = makeAddr("RepairFund");
LendingPool lendingPool;
StabilityPool stabilityPool;
RAACHousePrices raacHousePrices;
crvUSDToken crvUSD;
RToken rToken;
DebtToken debtToken;
DEToken deToken;
RAACToken raacToken;
RAACNFT raacNft;
veRAACToken veRaacToken;
RAACMinter raacMinter;
FeeCollector feeCollector;
Treasury treasury;
function setUp() public {
vm.warp(1 days);
raacHousePrices = new RAACHousePrices(owner);
// Deploy tokens
raacToken = new RAACToken(owner, 100, 50);
veRaacToken = new veRAACToken(address(raacToken));
crvUSD = new crvUSDToken(owner);
rToken = new RToken("RToken", "RToken", owner, address(crvUSD));
debtToken = new DebtToken("DebtToken", "DT", owner);
raacNft = new RAACNFT(address(crvUSD), address(raacHousePrices), owner);
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
// Deploy Treasury and FeeCollector
treasury = new Treasury(owner);
feeCollector = new FeeCollector(
address(raacToken),
address(veRaacToken),
address(treasury),
repairFund,
owner
);
// Deploy LendingPool
lendingPool = new LendingPool(
address(crvUSD),
address(rToken),
address(debtToken),
address(raacNft),
address(raacHousePrices),
0.1e27
);
lendingPool.transferOwnership(owner);
// Deploy stabilityPool Proxy
bytes memory data = abi.encodeWithSelector(
StabilityPool.initialize.selector,
address(rToken),
address(deToken),
address(raacToken),
address(owner),
address(crvUSD),
address(lendingPool)
);
address stabilityPoolProxy = address(
new TransparentUpgradeableProxy(
address(new StabilityPool(owner)),
owner,
data
)
);
stabilityPool = StabilityPool(stabilityPoolProxy);
// RAACMinter
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
owner
);
// Initialization
vm.startPrank(owner);
raacHousePrices.setOracle(owner);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
stabilityPool.setRAACMinter(address(raacMinter));
lendingPool.setStabilityPool(address(stabilityPool));
raacToken.setMinter(address(raacMinter));
raacToken.setFeeCollector(address(feeCollector));
raacToken.manageWhitelist(address(feeCollector), true);
raacToken.manageWhitelist(address(raacMinter), true);
raacToken.manageWhitelist(address(stabilityPool), true);
raacToken.manageWhitelist(address(veRaacToken), true);
vm.stopPrank();
vm.label(address(crvUSD), "crvUSD");
vm.label(address(rToken), "RToken");
vm.label(address(debtToken), "DebtToken");
vm.label(address(deToken), "DEToken");
vm.label(address(raacToken), "RAACToken");
vm.label(address(raacNft), "RAAC NFT");
vm.label(address(lendingPool), "LendingPool");
vm.label(address(stabilityPool), "StabilityPool");
vm.label(address(raacMinter), "RAACMinter");
vm.label(address(veRaacToken), "veRAAC");
vm.label(address(feeCollector), "FeeCollector");
vm.label(address(treasury), "Treasury");
vm.label(repairFund, "RepairFund");
}
function testAudit_MultipleLocks() public {
address alice = makeAddr("Alice");
deal(address(raacToken), alice, 200e18); // 110M
// Alice locks twice
vm.startPrank(alice);
raacToken.approve(address(veRaacToken), 200e18);
veRaacToken.lock(100e18, 365 days);
veRaacToken.lock(100e18, 365 days);
vm.stopPrank();
vm.warp(block.timestamp + 365 days);
console.log(raacToken.balanceOf(alice));
// Alice withdraw
vm.prank(alice);
veRaacToken.withdraw();
// Only 100 RAAC token received back
assertEq(raacToken.balanceOf(alice), 100e18);
}
}

Tools Used

Manual Review

Recommendations

When lock() is called, should check if user has existing deposits, then refund the RAAC tokens or simply revert.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

veRAACToken::lock called multiple times, by the same user, leads to loss of funds

Support

FAQs

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

Give us feedback!