stake.link

stake.link
DeFiHardhatBridge
27,500 USDC
View results
Submission Details
Severity: medium
Invalid

Unable to use TransferToken() function cause of its OnlyOwner modifier

Summary

Unable to use TransferToken() function cause of its OnlyOwner modifier

Vulnerability Details

As stakeLink documentation says in contest page ;

WrappedTokenBridge
The wrapped token bridge handles the transfer of liquid staking tokens (stLINK) between the primary and secondary chains.
Specifically it allows a user to wrap stLINK into wstLINK and send it to a secondary chain in a single tx which is necessary because stLINK only exists as
wstLINK on secondary chains due to its rebasing nature. Similarly, it also allows a user to transfer wstLINK from a secondary chain to the primary chain and
receive stLINK on the primary chain in a single tx.

a user should be able to use this transferToken function however wont be able to cause of the modifier.

Click to see hardhat test suit contract

import { ethers } from 'hardhat'
import { assert, expect } from 'chai'
import { toEther, deploy, deployUpgradeable, getAccounts, fromEther } from '../../utils/helpers'
import {
ERC677,
StrategyMock,
StakingPool,
WrappedSDToken,
WrappedTokenBridge,
CCIPOnRampMock,
CCIPOffRampMock,
CCIPTokenPoolMock,
WrappedNative,
} from '../../../typechain-types'
import { Signer } from 'ethers'
describe('WrappedTokenBridge', () => {
let linkToken: ERC677
let token2: ERC677
let wrappedToken: WrappedSDToken
let stakingPool: StakingPool
let bridge: WrappedTokenBridge
let onRamp: CCIPOnRampMock
let offRamp: CCIPOffRampMock
let tokenPool: CCIPTokenPoolMock
let tokenPool2: CCIPTokenPoolMock
let wrappedNative: WrappedNative
let accounts: string[]
let signers:Signer[]
before(async () => {
;({ signers,accounts } = await getAccounts())
})
beforeEach(async () => {
linkToken = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677
token2 = (await deploy('ERC677', ['2', '2', 1000000000])) as ERC677
stakingPool = (await deployUpgradeable('StakingPool', [
linkToken.address,
'Staked LINK',
'stLINK',
[],
])) as StakingPool
wrappedToken = (await deploy('WrappedSDToken', [
stakingPool.address,
'Wrapped stLINK',
'wstLINK',
])) as WrappedSDToken
const strategy = (await deployUpgradeable('StrategyMock', [
linkToken.address,
stakingPool.address,
toEther(100000),
toEther(0),
])) as StrategyMock
await stakingPool.addStrategy(strategy.address)
await stakingPool.setPriorityPool(accounts[0])
await stakingPool.setRewardsInitiator(accounts[0])
await linkToken.approve(stakingPool.address, ethers.constants.MaxUint256)
await stakingPool.deposit(accounts[0], toEther(10000))
await stakingPool.deposit(accounts[1], toEther(2000))
await linkToken.transfer(strategy.address, toEther(12000))
await stakingPool.updateStrategyRewards([0], '0x')
wrappedNative = (await deploy('WrappedNative')) as WrappedNative
const armProxy = await deploy('CCIPArmProxyMock')
const router = await deploy('Router', [wrappedNative.address, armProxy.address])
tokenPool = (await deploy('CCIPTokenPoolMock', [wrappedToken.address])) as CCIPTokenPoolMock
tokenPool2 = (await deploy('CCIPTokenPoolMock', [token2.address])) as CCIPTokenPoolMock
onRamp = (await deploy('CCIPOnRampMock', [
[wrappedToken.address, token2.address],
[tokenPool.address, tokenPool2.address],
linkToken.address,
])) as CCIPOnRampMock
offRamp = (await deploy('CCIPOffRampMock', [
router.address,
[wrappedToken.address, token2.address],
[tokenPool.address, tokenPool2.address],
])) as CCIPOffRampMock
await router.applyRampUpdates([[77, onRamp.address]], [], [[77, offRamp.address]])
bridge = (await deploy('WrappedTokenBridge', [
router.address,
linkToken.address,
stakingPool.address,
wrappedToken.address,
])) as WrappedTokenBridge
await linkToken.approve(bridge.address, ethers.constants.MaxUint256)
await stakingPool.approve(bridge.address, ethers.constants.MaxUint256)
})
it('getFee should work correctly', async () => {
assert.equal(fromEther(await bridge.getFee(77, false)), 2)
assert.equal(fromEther(await bridge.getFee(77, true)), 3)
await expect(bridge.getFee(78, false)).to.be.reverted
await expect(bridge.getFee(78, true)).to.be.reverted
})
it('transferTokens should work correctly with LINK fee', async () => {
let preFeeBalance = await linkToken.balanceOf(accounts[0])
await bridge.connect(signers[1]).transferTokens(77, accounts[4], toEther(100), false, toEther(10))
let lastRequestData = await onRamp.getLastRequestData()
let lastRequestMsg = await onRamp.getLastRequestMessage()
assert.equal(fromEther(await wrappedToken.balanceOf(tokenPool.address)), 50)
assert.equal(fromEther(preFeeBalance.sub(await linkToken.balanceOf(accounts[0]))), 2)
assert.equal(fromEther(lastRequestData[0]), 2)
assert.equal(lastRequestData[1], bridge.address)
assert.equal(
ethers.utils.defaultAbiCoder.decode(['address'], lastRequestMsg[0])[0],
accounts[4]
)
assert.equal(lastRequestMsg[1], '0x')
assert.deepEqual(
lastRequestMsg[2].map((d) => [d.token, fromEther(d.amount)]),
[[wrappedToken.address, 50]]
)
assert.equal(lastRequestMsg[3], linkToken.address)
await expect(
bridge.transferTokens(77, accounts[4], toEther(100), false, toEther(1))
).to.be.revertedWith('FeeExceedsLimit()')
})

run test with

npx hardhat test --network hardhat --grep 'transferTokens should work correctly with LINK fee'

test will fail as

Error: VM Exception while processing transaction: reverted with reason string 'Ownable: caller is not the owner'

Impact

Users wont be able to send wrapped tokens with native payment method cause onlyOwner nodifier.

Tools Used

hardhat-manuel review

Recommendations

OnlyOwner modifier should be removed .

P.S.:These assumptions made cause of the documentation

Updates

Lead Judging Commences

0kage Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
torpedopistolixc41 Submitter
over 1 year ago
0kage Lead Judge
over 1 year ago
0kage Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.