Upgradable contract assembler

Hey,

I’ve recently been going over my understanding of transparent proxies, and noticed that at some-point there’s been a change:

Old code from OZ blog post

assembly {
  let ptr := mload(0x40)

  // (1) copy incoming call data
  calldatacopy(ptr, 0, calldatasize)

  // (2) forward call to logic contract
  let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
  let size := returndatasize

  // (3) retrieve return data
  returndatacopy(ptr, 0, size)

  // (4) forward return data back to caller
  switch result
  case 0 { revert(ptr, size) }
  default { return(ptr, size) }
}

New code from OZ contract:

assembly {
  // (1) copy incoming call data
  calldatacopy(0, 0, calldatasize())

  // (2) forward call to logic contract
  let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)

  // (3) retrieve return data
  returndatacopy(0, 0, returndatasize())

  // (4) forward return data back to caller
  switch result
  case 0 { revert(0, returndatasize()) }
  default { return(0, returndatasize()) }
}

I know that let ptr := mload(0x40) used to be a special-slot containing the address of the next allocatable slot, so it made sense to me that it was used as the reference slot for some of these OPs, but now they use 0, is this some kind of shortcut which has been introduced? Or is there some assumption being made that 0 is the slot where the delegatcall will return its value?

P.S. This is a dupe from a post I made in security a few days ago, I now realise it should have been posted here - but I don’t appear to be able to delete my previous post.

1 Like

Welcome to the forum @aftermath.digital. I’ve deleted your previous post.

This was changed because it is cheaper this way, and it’s not necessary to write in the free memory pointer (mload(0x40)). It’s not necessary because we will return immediately after writing this memory, so we don’t have to preserve any Solidity invariants and we can just write to memory starting at position 0.

There is no assumption made, delegatecall returns its value to a separate “returndata” memory space, and we use returndatacopy to copy it over to the general memory space, starting at position 0 because of what I mentioned in the previous paragraph.

2 Likes

Just double-checking that it is the first four 32-byte slots reserved by Solidity, which you referred to? Or only the first two?

It would always be okay to write in the first two words. In the two words after that, it would generally be wrong, but in this case we're not returning to Solidity code, so we can write over the four.

1 Like

Yo,

I should really have responded to this myself.

The way I understand it is that at present solidity only uses those first two slots to facilitate hashing, so you can always use them yourself as solidity itself will clobber and use them immediately should the need arise.

So if you store something in there and then perform some hashing functionality at the solidity level, you'll be clobbered, but you shouldn't ever impair the solidity run-time by using them.