Platform: Code4rena
Start Date: 21/08/2023
Pot Size: $125,000 USDC
Total HM: 26
Participants: 189
Period: 16 days
Judge: GalloDaSballo
Total Solo HM: 3
Id: 278
League: ETH
Rank: 33/189
Findings: 1
Award: $638.64
🌟 Selected for report: 1
🚀 Solo Findings: 0
638.6354 USDC - $638.64
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1016
When the admin pauses the contract RdpxV3core locks anyone to use this contract such as a bond, bondWithDelegate, and withdraw. But a function redeem is still available to use. Any situations that occur like a hack accident or system are under maintenance then anyway, your smart contract is running to continue to get unexpected.
The code below does not call a function whenNotPause()
to check whether this contract is paused or not.
function redeem( uint256 id, address to ) external returns (uint256 receiptTokenAmount) { // Validate bond ID _validate(bonds[id].timestamp > 0, 6); // Validate if bond has matured _validate(block.timestamp > bonds[id].maturity, 7); _validate( msg.sender == RdpxV2Bond(addresses.receiptTokenBonds).ownerOf(id), 9 ); // Burn the bond token // Note requires an approval of the bond token to this contract RdpxV2Bond(addresses.receiptTokenBonds).burn(id); // transfer receipt tokens to user receiptTokenAmount = bonds[id].amount; IERC20WithBurn(addresses.rdpxV2ReceiptToken).safeTransfer( to, receiptTokenAmount ); emit LogRedeem(to, receiptTokenAmount); }
To reproduce this vulnerability, let's modify a test called testRedeem()
in a /test/rdpxV2-core/Unit.t.sol
. After running the test, you will get everything passed. So it is proof of vulnerable
function testRedeem() public { rdpxV2Core.bond(1 * 1e18, 0, address(this)); // ... rdpxV2Core.pause(); // <- add this line to test // test redeem after expiry rdpxV2Bond.approve(address(rdpxV2Core), 0); rdpxV2Core.redeem(0, address(1)); assertEq(rdpxV2Bond.balanceOf(address(this)), 0); assertEq(rdpxV2ReceiptToken.balanceOf(address(1)), 25 * 1e16); // ... }
Manually
Adding the line _whenNotPaused();
into the function redeem()
.
Invalid Validation
#0 - c4-pre-sort
2023-09-10T07:02:35Z
bytes032 marked the issue as primary issue
#1 - bytes032
2023-09-10T07:02:43Z
I assuming this might be the intended behavior. Leaving for sponsor review.
#2 - c4-pre-sort
2023-09-10T07:03:11Z
bytes032 marked the issue as sufficient quality report
#3 - c4-sponsor
2023-09-25T16:44:47Z
psytama (sponsor) confirmed
#4 - c4-judge
2023-10-15T18:04:59Z
GalloDaSballo marked the issue as selected for report
#5 - 141345
2023-10-22T15:32:07Z
When a contract is paused, it means something goes wrong. redeem()
function is a way for users to exit, otherwise the fund is locked. Why isn't this intended behavior?
#6 - GalloDaSballo
2023-10-25T09:13:23Z
/** * @notice Lets the delegate withdraw unused WETH * @param delegateId The ID of the delegate * @return amountWithdrawn The amount of WETH withdrawn * */ function withdraw(uint256 delegateId) external returns (uint256 amountWithdrawn) { _whenNotPaused(); _validate(delegateId < delegates.length, 14); Delegate storage delegate = delegates[delegateId]; _validate(delegate.owner == msg.sender, 9); amountWithdrawn = delegate.amount - delegate.activeCollateral; _validate(amountWithdrawn > 0, 15); delegate.amount = delegate.activeCollateral; IERC20WithBurn(weth).safeTransfer(msg.sender, amountWithdrawn); emit LogDelegateWithdraw(delegateId, amountWithdrawn); }
I think there's enough of a doubt to make the finding valid