stake.link

stake.link
DeFiHardhatBridge
27,500 USDC
View results
Submission Details
Severity: high
Valid

Not Deleting Previous Approves in Cross-chain `reSDL` Transfer Lead to Steal of Funds

Summary

The SDLPool's cross-chain transfer mechanism does not revoke existing approvals of the reSDL NFT when transferred between chains. This oversight allows an original approver to exert control over an reSDL, even after it has been sold and its value potentially increased by new owners on other chains.

Vulnerability Details

  • The SDLPool allows users to stake SDL tokens and receive reSDL NFTs, which represent their staked positions. Each reSDL NFT is associated with a unique lockId that contains details such as the staked amount, boost amount, and duration...

  • While reSDL positions can be transferred cross-chain or within the same chain, a security gap exists in the cross-chain transfer mechanism. Specifically, when an reSDL is transferred from one chain to another, the previous approvals for the lockId on the source chain are not revoked as we can see in handleOutgoingRESDL function (in sdlPools cross all chains, secondaries and primary):

function handleOutgoingRESDL(address _sender, uint256 _lockId, address _sdlReceiver)
external
onlyCCIPController
onlyLockOwner(_lockId, _sender)
updateRewards(_sender)
updateRewards(ccipController)
returns (Lock memory)
{
Lock memory lock = locks[_lockId];
delete locks[_lockId].amount;
delete lockOwners[_lockId];
balances[_sender] -= 1;
uint256 totalAmount = lock.amount + lock.boostAmount;
effectiveBalances[_sender] -= totalAmount;
effectiveBalances[ccipController] += totalAmount;
// sending the token to the sdlPoolCcipController .
sdlToken.safeTransfer(_sdlReceiver, lock.amount);
emit OutgoingRESDL(_sender, _lockId);
return lock;
}

This oversight can be exploited by malicious actors.

poc example :

  • consider the following example :

  • Bob stakes SDL tokens on the primary chain and receives an reSDL with lockId = 1.

  • in the primary chain he approves a secondary address he controls, bobSecondAddress, for this reSDL .

function approve(address _to, uint256 _lockId) external {
address owner = ownerOf(_lockId);
if (_to == owner) revert ApprovalToCurrentOwner();
if (msg.sender != owner && !isApprovedForAll(owner, msg.sender)) revert SenderNotAuthorized();
tokenApprovals[_lockId] = _to;
emit Approval(owner, _to, _lockId);
}
  • Bob then transfers this reSDL to a secondaryPool in other chain through RESDLTokenBridge.

  • bob will make a deal with Alice in the secondary chain, so alice will buys this reSDL from Bob on the secondary chain (notice that alice may increase its value by staking additional SDL tokens)

  • Now,at any point, if the reSDL with lockId = 1 is transferred back to the primary chain—whether by Alice or a subsequent owner,Bob can immediately use bobSecondAddress to execute safeTransferFrom on the primary chain for this lockId.

function safeTransferFrom(
address _from,
address _to,
uint256 _lockId,
bytes memory _data
) public {
if (!_isApprovedOrOwner(msg.sender, _lockId)) revert SenderNotAuthorized();//@audit this will not revert
_transfer(_from, _to, _lockId);
if (!_checkOnERC721Received(_from, _to, _lockId, _data)) revert TransferToNonERC721Implementer();
}
  • This allows Bob to reclaim the reSDL, exploiting the fact that his approval on the primary chain was never revoked, even after multiple ownership changes on the secondary chain.

  • The risk is that Bob can unexpectedly seize the reSDL upon its return to the primary chain, regardless of any value added by Alice or others to this position, compromising the integrity of the cross-chain transfer system.

Impact

  • This vulnerability allows original approvers to reclaim reSDL tokens after cross-chain transfers, which can lead to asset loss for current holders.

Tools Used

manual review

Recommendations

function handleOutgoingRESDL(address _sender, uint256 _lockId, address _sdlReceiver)
external
onlyCCIPController
onlyLockOwner(_lockId, _sender)
updateRewards(_sender)
updateRewards(ccipController)
returns (Lock memory)
{
Lock memory lock = locks[_lockId];
delete locks[_lockId].amount;
delete lockOwners[_lockId];
balances[_sender] -= 1;
uint256 totalAmount = lock.amount + lock.boostAmount;
effectiveBalances[_sender] -= totalAmount;
effectiveBalances[ccipController] += totalAmount;
// sending the token to the sdlPoolCcipController .
sdlToken.safeTransfer(_sdlReceiver, lock.amount);
++ delete tokenApprovals[_lockId]
emit OutgoingRESDL(_sender, _lockId);
return lock;
}
Updates

Lead Judging Commences

0kage Lead Judge
almost 2 years ago
0kage Lead Judge almost 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

stale-approval

Support

FAQs

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