Platform: Code4rena
Start Date: 29/07/2021
Pot Size: $20,000 USDC
Total HM: 8
Participants: 12
Period: 3 days
Judge: LSDan
Total Solo HM: 2
Id: 24
League: ETH
Rank: 6/12
Findings: 4
Award: $1,344.95
π Selected for report: 2
π Solo Findings: 0
π Selected for report: GalloDaSballo
838.3068 USDC - $838.31
GalloDaSballo
The function swapYieldSource
https://github.com/pooltogether/swappable-yield-source/blob/89cf66a3e3f8df24a082e1cd0a0e80d08953049c/contracts/SwappableYieldSource.sol#L307
Can be called by the owner (deployer / initializer) or Asset Manager
The function will take all funds from the old Yield Source, and transfer them to the new Yield source.
Any contract that implement the function function depositToken() external returns (address)
will pass the check
However, if either the owner or the assetManager have malicious intent, this function allows them to instantly rug all funds
function depositToken() external returns (address)
setYieldSource
while pointing at your malicious contractI highly recommend checking that the YieldSource is from a trusted registry before allowing this swap.
Alternatively forcing each Owner to be a TimeLock with at least 48 hours may provide enough security to allow this to be used in practice
#0 - PierrickGT
2021-08-11T22:11:14Z
This is why we will use a multi sig owned by governance to deploy swappable yield sources and manage them. This way, we will avoid these kind of scenarios.
#1 - 0xean
2021-08-24T18:04:44Z
Agree with warden on the risk here. Will both the AssetManager and the Owner be owned by your governance?
The YieldSource could easily extract user funds or send them back to the SwappableYieldSource contract and then remove them from there.
#2 - PierrickGT
2021-08-30T23:22:11Z
We have removed the AssetManager
role and Owner
will be owned by governance who will vet any change of yield source before going through a vote.
π Selected for report: tensors
Also found by: GalloDaSballo, cmichel, hickuphh3
169.7571 USDC - $169.76
GalloDaSballo
When swapping from one yield source to another, via the function _setYieldSource https://github.com/pooltogether/swappable-yield-source/blob/89cf66a3e3f8df24a082e1cd0a0e80d08953049c/contracts/SwappableYieldSource.sol#L254
You are correctly giving approval to the new yield source. However, since you are retiring the old one, it's probably best to remove the allowance from the old yield source
Change
function _setYieldSource(IYieldSource _newYieldSource) internal { _requireDifferentYieldSource(_newYieldSource); require(_newYieldSource.depositToken() == yieldSource.depositToken(), "SwappableYieldSource/different-deposit-token"); yieldSource = _newYieldSource; IERC20Upgradeable(_newYieldSource.depositToken()).safeApprove(address(_newYieldSource), type(uint256).max); emit SwappableYieldSourceSet(_newYieldSource); }
To
function _setYieldSource(IYieldSource _newYieldSource) internal { _requireDifferentYieldSource(_newYieldSource); require(_newYieldSource.depositToken() == yieldSource.depositToken(), "SwappableYieldSource/different-deposit-token"); IERC20Upgradeable(yieldSource.depositToken()).safeApprove(address(_newYieldSource), 0); yieldSource = _newYieldSource; IERC20Upgradeable(_newYieldSource.depositToken()).safeApprove(address(_newYieldSource), type(uint256).max); emit SwappableYieldSourceSet(_newYieldSource); }
#0 - PierrickGT
2021-08-06T16:08:39Z
π Selected for report: GalloDaSballo
310.484 USDC - $310.48
GalloDaSballo
Detailed description of the impact of this finding.
_requireYieldSource https://github.com/pooltogether/swappable-yield-source/blob/89cf66a3e3f8df24a082e1cd0a0e80d08953049c/contracts/SwappableYieldSource.sol#L74
Runs a few checks to see if the function depositToken
is implemented
Notice that this is not a guarantee that the target is a valid Yield Source.
This will simply verify that the contract has that method.
Any malicious attacker could implement that function and then set up the Yield Source to steal funds
In order to guarantee that the target is a valid Yield Source, you'd want to create a registry of know Yield Sources, perhaps controlled by governance or by the DAO, and check against that.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
Create any contract with just a function depositToken returns (address)
and you'll be able to add pass the check
Create an on-chain registry of known Yield Sources, either by committee or governance, and use a check against the registry, this will avoid griefing
#0 - PierrickGT
2021-08-11T21:43:14Z
Swappable Yield Sources will be deployed by a multi sig owned by governance, _requireYieldSource
function does indeed simply performs a sanity check to be sure that the yield source address passed is implementing the depositToken
function. This is to avoid any human error and deploying a swappable yield source that would be unusable cause the address passed wouldn't be a yield source.
Deployments of a new swappable yield source will be voted by governance, as will a change of yield source, so it would be pretty time and gas consuming to have also to add any new yield source we which to switch to to a registry,
#1 - 0xean
2021-08-24T17:07:20Z
Agree with warden that these checks are not sufficient to deter a malicious implementation. Additionally, switching of the yield source looks to be feasible by the owner (presumably the above mentioned multisig) or the AssetManager which is unclear who controls this address. Leaving open.
#2 - PierrickGT
2021-08-30T23:23:11Z
We have removed the AssetManager
role and Owner
will be owned by governance who will vet any change of yield source before going through a vote.
π Selected for report: hickuphh3
Also found by: 0xRajeev, GalloDaSballo, cmichel, pauliax
26.3992 USDC - $26.40
GalloDaSballo
In supplyTokenTo
, redeemToken
, approveMaxAmount
, _setYieldSource
, and _transferFunds
you write:
_yieldSource.depositToken()
, however you already defined a public
function called depositToken
to retrieve the address of the depositToken for the current Yield Source.
Since it's marked public, you could refactor every use of _yieldSource.depositToken()
Β to depositToken()
Otherwise, you can mark the function as external
so that it's clear it's just supposed to be used from outside
#0 - PierrickGT
2021-08-06T16:31:41Z