Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Reentrancy-Guard modifier to external MP methods #1334

Merged
merged 39 commits into from
May 5, 2020

Conversation

nicholaspai
Copy link
Member

@nicholaspai nicholaspai commented Apr 29, 2020

Fixes #1332
Fixes #1311

This leaves us with the following bytecode statistics:

Contract is 24235 bytes in size.
This leaves a total of 341 bytes within the EIP170 limit.

nicholaspai and others added 9 commits April 28, 2020 08:40
…quest and updated tests

Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Christopher Maree <christopher.maree@gmail.com>
Signed-off-by: Christopher Maree <christopher.maree@gmail.com>
@nicholaspai nicholaspai added the tech debt Technical Debt label Apr 29, 2020
@nicholaspai nicholaspai added this to the Expiring Multiparty milestone Apr 29, 2020
.circleci/config.yml Outdated Show resolved Hide resolved
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
address internal finderAddress;

constructor(address _finderAddress) public {
finderAddress = _finderAddress;
}

function _registerContract(address[] memory parties, address contractToRegister) internal {
function _registerContract(address[] memory parties, address contractToRegister) internal nonReentrant() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added because of call to registry.registerContract

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So just to double-check: reentrancy would occur here if the party registered was a smart contract with a hook it could re-enter at this point? what would this enable it to do within the context of the _registerContract function?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably overkill, I was just putting nonReentrant on functions that made external calls. The reetrancy would be if the registered Registry address in the Finder was a smart contract that attempted to re-enter.

I can't think of an actual exploit they would do, but I suppose re-entrancy is possible.

        Registry registry = Registry(finder.getImplementationAddress(OracleInterfaces.Registry));
        registry.registerContract(parties, contractToRegister);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will remove though because its not an external method

Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
@nicholaspai
Copy link
Member Author

After discussions with the team, we concluded that in a perfect world we would add reentrancy-guards to all external methods, including view-only methods with the nonReetrantView() modifier. However, I'm running into practical limits with our contract deployed bytecode.

Currently, there are external methods in AddressWhitelist, SyntheticToken, PricelessPositionManager, and ExpiringMultiPartyCreator (and potentially elsewhere) that are no yet guarded by these modifiers. This is because doing so soon pushes the contract over the bytecode size.

As of the time of posting, ExpiringMultiPartyCreator is ~400 bytes short of the limit.

@nicholaspai nicholaspai changed the title Add Reentrancy-Guard modifier to EMP methods that call external methods Add Reentrancy-Guard modifier to external MP methods Apr 30, 2020
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
…ctors, has 94 bytes of bytecode left

Signed-off-by: Nick Pai <npai.nyc@gmail.com>
@nicholaspai
Copy link
Member Author

I've added nonReentrant() to all state-modifying public methods in the EMP (NOT including any contracts in the common/ folder such as AddressWhitelist.sol.

This leaves the EMPCreator with < 100 bytes of bytecode remaining.

I'd still like to find a way to add nonReentrant() to the contracts in common/ that are exclusively used by the EMP, the constructors in the EMP, and adding nonReentrantView() to all public view-only methods. But the main obstacle right now is the bytecode limit.

@nicholaspai nicholaspai marked this pull request as ready for review May 1, 2020 15:37
Comment on lines 32 to 44
modifier nonReentrant() {
// On the first call to nonReentrant, _notEntered will be true
require(_notEntered, "ReentrancyGuard: reentrant call");

// Any calls to nonReentrant after this point will fail
_notEntered = false;

_;

// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_notEntered = true;
}
Copy link
Member

@mrice32 mrice32 May 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it help the bytecode size if we put all the logic in two (or just one) internal methods _preEntranceCheck() and _postEntranceReset() (the latter may not help/be necessary)? Obviously, it's messier, but it just wanted to try it in case it would slightly improve our bytecode issues (my worry is that the require call is taking a lot of bytecode and being copied around to every method).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this have the same number of require statements per function call?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic would be the same. But the way Solidity organizes the bytecode may be different. For instance, I think modifiers generally work this way:

function 1 start
modifier bytecode1 ( pre _; )
function 1 logic bytecode
modifier bytecode2 ( post _; )
function 1 end

function 2 start
modifier bytecode1 ( pre _; )
function 2 logic bytecode
modifier bytecode2 ( post _; )
function 2 end

...

The modifier bytecode would just be pasted into every function.
If you were to have the modifier call a function, it might look like this:

function 1 start
JUMP to modifier bytecode1
function 1 logic bytecode
JUMP to modifier bytecode2
function 1 end

function 1 start
JUMP to modifier bytecode1
function 1 logic bytecode
JUMP to modifier bytecode2
function 1 end

...

modifier bytecode1 (pre _; )
modifier bytecode2 (post _; )

In the latter, there's only one instruction to run each part of the modifier bytecode -- jumping to the function that the modifier calls. In the former, the entire modifier bytecode will be pasted for each function, bloating the bytecode. All the opcodes and data that make up that require statement may only be in one place in the bytecode rather than copied into every function. We've established in the past that require statements have a pretty big impact on the bytecode (especially when they have a message), so I thought eliminating the repetition may help -- although I'm not 100% sure of how solidity organizes this. It could require you changing the number of runs in the optimizer to see the impact.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok this would help, but the overall bytecode impact would be small, likely < 1000 bytes. We'll run into the bytecode limit either way once we add back require messages. I think we should think bigger picture about how to organize the code to save bytecode.

I do think this change would help some and I'll test it out.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This saved ~800 bytes, great idea @mrice32 !

Signed-off-by: Nick Pai <npai.nyc@gmail.com>
@nicholaspai
Copy link
Member Author

I've now applied the lock to all public methods including view-only methods. The one notable exception is pfc() which we cannot make nonReentrantView() because it is called by payRegularFees()

Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
@mrice32
Copy link
Member

mrice32 commented May 1, 2020

I've now applied the lock to all public methods including view-only methods. The one notable exception is pfc() which we cannot make nonReentrantView() because it is called by payRegularFees()

Seems reasonable. You could technically create an internal and external version of pfc to mitigate this issue, but since it's not really used for anything outside of the contract (only used for informational purposes), I don't really see a reason to do so, especially since that would give us less bytecode space to work with :)

@nicholaspai
Copy link
Member Author

I've now applied the lock to all public methods including view-only methods. The one notable exception is pfc() which we cannot make nonReentrantView() because it is called by payRegularFees()

Seems reasonable. You could technically create an internal and external version of pfc to mitigate this issue, but since it's not really used for anything outside of the contract (only used for informational purposes), I don't really see a reason to do so, especially since that would give us less bytecode space to work with :)

Yeah I thought about this too, it comes down to consistency versus bytecode. I'd opt for saving bytecode and I'll include a comment detailing why this public view function does not have t he modifier.

Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
@mrice32
Copy link
Member

mrice32 commented May 4, 2020

I've now applied the lock to all public methods including view-only methods. The one notable exception is pfc() which we cannot make nonReentrantView() because it is called by payRegularFees()

Seems reasonable. You could technically create an internal and external version of pfc to mitigate this issue, but since it's not really used for anything outside of the contract (only used for informational purposes), I don't really see a reason to do so, especially since that would give us less bytecode space to work with :)

Yeah I thought about this too, it comes down to consistency versus bytecode. I'd opt for saving bytecode and I'll include a comment detailing why this public view function does not have t he modifier.

SGTM.

Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Copy link
Member

@mrice32 mrice32 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! A few minor comments.

require(_notEntered, "ReentrancyGuard: reentrant call");
}

function _postEntranceSet() internal {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: shouldn't this be _preEntranceSet()? Since it's before you enter the function? Or am I misunderstanding the naming convention?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep typo on my part

constructor(ConstructorParams memory params)
public
Liquidatable(params)
// nonReentrant() This modifier is already applied on the FeePayer constructor.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Shouldn't the guard be applied at the outer-most layer rather than the inner most? Otherwise, can't you have re-entrancy once you leave the inner-most call?
  2. Does this fail if you add this here? Isn't the Liquidatable constructor called before the re-entrancy guard goes into effect?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I think your counterexample is correct. I applied it on the innermost because I didn't want someone to construct a PricelessPositionManager.sol or Liquidatable.sol whose constructor wasn't guarded by nonReentrant(). In practice this might not be super useful since we are forcing everyone to deploy EMP.sol's.
  2. It doesn't actually fail if I add to FeePayer AND EMP, even though the bytecode does change (I verified). I ended up adding to just the top-level one in ExpiringMultiParty.sol.

@@ -156,6 +156,7 @@ contract Liquidatable is PricelessPositionManager {
params.minSponsorTokens,
params.timerAddress
)
// nonReentrant() This modifier is already applied on the FeePayer constructor.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above.

)
public
FeePayer(_collateralAddress, _finderAddress, _timerAddress)
// nonReentrant() This modifier is already applied on the FeePayer constructor.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above.

Comment on lines 580 to 582
* Note: We do not apply the nonReentrantView() modifier here because it is applied to `payRegularFees()` which will call `pfc()`.
* We could have replaced added an internal `_pfc()` method to be called by `payRegularFees()` in order to make this `nonReentrantView()`,
* but decided not to in order to save bytecode.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we have the bytecode issue solved, should we do this just for consistency? Not super opinionated either way, but just wanted to mention it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah thanks for the reminder. I added the modifier back and it ended up adding 25 bytes to the deployedBytecode FYI.

Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Copy link
Member

@mrice32 mrice32 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks awesome -- one minor comment.

@@ -5,5 +5,5 @@ import "./Liquidatable.sol";


contract ExpiringMultiParty is Liquidatable {
constructor(ConstructorParams memory params) public Liquidatable(params) {}
constructor(ConstructorParams memory params) public Liquidatable(params) nonReentrant() {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does nonReentrant() go into effect before or after the Liquidatable constructor gets called? Is it possible to put this modifier before the Liquidatable constructor is called? Or if it's not, maybe it makes sense to add nonReentrant() to all the constructors (since they shouldn't interfere with one another)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructors do not appear to interfere with each other so I just went ahead and put it on all of them.

Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Copy link
Member

@mrice32 mrice32 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One minor nit.

*/
function pfc() public override view returns (FixedPoint.Unsigned memory) {
return super.pfc().add(_getFeeAdjustedCollateral(rawLiquidationCollateral));
function pfc() public override view nonReentrantView() returns (FixedPoint.Unsigned memory) {
Copy link
Member

@mrice32 mrice32 May 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: why not just implement this in FeePayer? Then all the derived contracts would have to implement is _pfc()? Just feels a little weird implementing this here since there's nothing barring the base class from just implementing it once.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done good point

Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Copy link
Member

@mrice32 mrice32 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@nicholaspai nicholaspai merged commit f561463 into master May 5, 2020
@nicholaspai nicholaspai deleted the npai/nfct-reentrancy-guard branch May 5, 2020 19:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
tech debt Technical Debt
Projects
None yet
3 participants