Platform: Code4rena
Start Date: 02/08/2023
Pot Size: $42,000 USDC
Total HM: 13
Participants: 45
Period: 5 days
Judge: hickuphh3
Total Solo HM: 5
Id: 271
League: ETH
Rank: 7/45
Findings: 2
Award: $1,121.02
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: Aymen0909
Also found by: 0xStalin, 0xbepresent, Arz, D_Auditor, Jorgect, T1MOH, bin2chen, dirk_y, josephdara, ptsanev, rvierdiiev, seerether, shirochan, trachev
89.6296 USDC - $89.63
RngRelayAuction::rngComplete()
function and sending data at their convenienceThe RngRelayAuction::rngComplete()
is on charge of closing the draw in the PrizePool as well as withdrawing the reserve and send it to the rewardRecipient.
All the data that should be used to execute the rngComplete()
function should be computed by the relayer, this function forwards the computed data from the relayer onto the PrizePool.
The problem is that this function has no access control, there is any check to validate if the caller is the relayer, which means that anybody can call this function and set the parameters at their convenience, which will lead to close draws using values that could benefit the caller and all the rest of users will be impacted because the randomNumber is now manipulated at the convenience of whoever calls first the rngComplete()
.
Manual Audit
RngRelayAuction::rngComplete()
rngAuctionRelayer
variable which as stated by the comments in the code, this variable stores the address of the relayer that RNG results must originate from.function rngComplete( ... ) external returns (bytes32) { + require(msg.sender == rngAuctionRelayer, "Caller is not the authorized relayer"); ... ... ... }
Invalid Validation
#0 - c4-pre-sort
2023-08-08T01:37:44Z
raymondfam marked the issue as low quality report
#1 - raymondfam
2023-08-08T01:38:09Z
The function deals with random numbers that had already been generated.
#2 - c4-pre-sort
2023-08-08T02:29:48Z
raymondfam marked the issue as remove high or low quality report
#3 - c4-pre-sort
2023-08-08T02:30:03Z
raymondfam marked the issue as duplicate of #82
#4 - c4-judge
2023-08-14T02:44:55Z
HickupHH3 marked the issue as satisfactory
🌟 Selected for report: 0xStalin
Also found by: MohammedRizwan
1031.3943 USDC - $1,031.39
LiquidationPairFactory::createPair()
allows the callers to set the LiquidationSource as any arbitrary address with no restrictions._source
parameter may not necessarily be a real vault contract, and even though the _source
address is set as a fake malicious contract, the LiquidationPair will be created and added to the deployedPairs
mapping, and as stated in the protocol's documentation, any LiquidationPair created by the factory determines if a pair is legitimate or notSo, if a LiquidationPair is created by the LiquidationPairFactory may allow malicious users to trick users who want to liquidate the Vault's Yield to operate with a LiquidationPair who'll end up stealing their POOL tokens (tokenIn) when swapping tokens.
Practical Example of how the LiquidationPair would steal the user's assets, (Keep in mind that source
is not the address of a Vault, but an arbitrary contract)
source
contract would look like this:contract FakeSource { function targetOf(address _token) external view returns (address) { return <ContractCreatorAddress>; } function liquidate( address _account, address _tokenIn, uint256 _amountIn, address _tokenOut, uint256 _amountOut ) public virtual override returns (bool) { return true; } }
LiquidationPair.target()
will return source.targetOf(tokenIn)
, and the source
contract can return any address when the targetOf()
is called, so, let's say that will return the address of its creator.LiquidationPair.target()
will return the address of its creator, instead of returning the expected address of the PrizePool
contractLiquidationPair::swapExactAmount()
will do some computations prior to call source.liquidate()
, and the source.liquidate()
can just return true not to cause the tx to be reverted.So, LiquidationPair::swapExactAmount()
will basically do nothing.
With the above points in mind, let's see what would be the result of swapping using the LiquidationRouter contract
function swapExactAmountOut( LiquidationPair _liquidationPair, address _receiver, uint256 _amountOut, uint256 _amountInMax ) external onlyTrustedLiquidationPair(_liquidationPair) returns (uint256) { //@audit-issue => The `tokenIn` will be transferred to the address of the LiquidationPair's creator instead of the PrizePool contract (Point 1) <====== Point 1 ======> IERC20(_liquidationPair.tokenIn()).safeTransferFrom( msg.sender, _liquidationPair.target(), _liquidationPair.computeExactAmountIn(_amountOut) ); //@audit-issue => This call will basically do nothing, just return a true not to cause the tx to be reverted (Point 2) <====== Point 2 ======> uint256 amountIn = _liquidationPair.swapExactAmountOut(_receiver, _amountOut, _amountInMax); emit SwappedExactAmountOut(_liquidationPair, _receiver, _amountOut, _amountInMax, amountIn); return amountIn; }
Manual Audit
deployedVaults
mapping of the VaultFactory
contract to validate if the inputted address of the _source
parameter is a valid vault supported by the Protocol.
_tokenIn
and _tokenOut
by pulling the values that are already set up in the vault.function createPair( ILiquidationSource _source, - address _tokenIn, - address _tokenOut, uint32 _periodLength, uint32 _periodOffset, uint32 _targetFirstSaleTime, SD59x18 _decayConstant, uint112 _initialAmountIn, uint112 _initialAmountOut, uint256 _minimumAuctionAmount ) external returns (LiquidationPair) { + require(VaultFactory.deployedVaults(address(_source)) == true, "_source address is not a supported Vault"); + address _prizePool = _source.prizePool(); + address _tokenIn = _prizePool.prizeToken(); + address _tokenOut = address(_source); LiquidationPair _liquidationPair = new LiquidationPair( _source, _tokenIn, _tokenOut, _periodLength, _periodOffset, _targetFirstSaleTime, _decayConstant, _initialAmountIn, _initialAmountOut, _minimumAuctionAmount ); allPairs.push(_liquidationPair); deployedPairs[_liquidationPair] = true; emit PairCreated( _liquidationPair, _source, _tokenIn, _tokenOut, _periodLength, _periodOffset, _targetFirstSaleTime, _decayConstant, _initialAmountIn, _initialAmountOut, _minimumAuctionAmount ); return _liquidationPair; }
Invalid Validation
#0 - c4-pre-sort
2023-08-08T02:56:33Z
raymondfam marked the issue as duplicate of #52
#1 - c4-pre-sort
2023-08-08T05:59:06Z
raymondfam marked the issue as high quality report
#2 - c4-pre-sort
2023-08-10T00:00:14Z
raymondfam marked the issue as not a duplicate
#3 - c4-pre-sort
2023-08-10T00:00:31Z
raymondfam marked the issue as primary issue
#4 - asselstine
2023-08-10T16:37:25Z
This is an interesting report.
There are two ways we expect liquidations to occur:
Smart contracts that interact with the Pair can easily assert that they received the expected tokens; so a malicious LiquidationSource would not be a problem.
However, EOAs that interact with a Pair via the LiquidationRouter would be vulnerable to this kind of attack.
This makes me think that the fix is actually to alter the LiquidationRouter so that:
swapExactAmountOut
the router makes itself the recipient of the tokens.It costs more gas, but this change ensures that anyone who calls the router would receive the expected tokens.
#5 - c4-sponsor
2023-08-10T16:37:31Z
asselstine marked the issue as sponsor confirmed
#6 - c4-judge
2023-08-14T06:41:28Z
HickupHH3 marked the issue as selected for report