Preamble
VIP: <to be assigned>
Title: reduce the cost and difficulty of code reuse in common situations
Author: maurelian
Status: Draft
Created: 2018-06-16
Requires (*optional): <VIP number(s)>
Replaces (*optional): <VIP number(s)>
Simple Summary
- I shouldn't need to copy and paste the same code snippets into 5 functions to valid authorization.
- I shouldn't feel like copying and pasting code is more efficient or cheaper.
Abstract
In order to call a within the same contract function, Vyper uses the CALL opcode, which send a new message to call functions within the same contract. This has some nice safety benefits.
Unfortunately it will result in developers using anti-patterns to save on gas, or access msg.sender in a function call.
Motivation
The benefit of using CALL to access code in the same contract is to create a new execution context, with no risk of side effects from memory access. I admit that I find this to be quite an elegant use of the EVM for safety.
Problems with using CALL
Unfortunately, there are two major issues with the use of the CALL opcode for calling functions within the contract:
1. Environment values change unexpectedly
At least two important environment opcodes will return different values: CALLER (ie. msg.sender) and CALLVALUE (ie. msg.value). This complicates the use of functions for permission checking. The following is a naive vyper translation of the common "ownable" pattern for a simple storage solidity contract.
owner: address
value: public(int128)
@public
def __init__():
self.owner = msg.sender
@private
def checkOwner():
assert msg.sender == self.owner # this will REVERT every time
@public
def writeValue(_value: int128):
self.checkOwner()
self.value = _value
2. Gas costs disincentivizes code reuse
The CALL opcode costs 700 gas (+ input data costs + function dispatching again). By contrast Solidity uses JUMP (2 gas) to call a function.
This imposes a large penalty on code reuse. Regardless Vyper's design philosophy favoring safety over gas efficiency, developers will respond to this incentive by simply copying and pasting boilerplate code. This is dangerous, and difficult to audit.
Specification:
I'm sorry, I don't have one specification. Below are some options I see for mitigating these issues.
From the Motivation section above, we have 3 parameters along which to analyze approaches to calling a function in the EVM.
- Gas cost
- Preservation of env variables
- Safety benefits
1. Use DELEGATECALL instead of CALL
- Gas cost: NEUTRAL (same as using CALL)
- Env variables: GOOD (gives us back
CALLER and CALLVALUE)
- Memory Safety: GOOD. It does indeed reset memory.
Seems like a decent solution!
3. Memory Safety: BAD (executes the code in the same memory context, nullifying reasons for using CALL in the first place) ~I proposed this earlier, but I don't think this is a good solution. ~
2. Just use JUMP like Solidity
- Gas cost: GOOD (Much lower gas cost)
- Env variables: GOOD (Preserves
CALLER and CALLVALUE)
- Memory Safety: BAD (Greater risk of compiler bugs)
This is worth considering.
See also #330.
3. Implement a safer analog to Solidity's modifiers
These could have significant restrictions on them, like no access to MSTORE or SSTORE, and not accepting arguments. I assume this would be done by with JUMP, or just inlining the same code each time it's needed.
- Gas cost: GOOD (Lower execution cost, but might increase size of contract if inlining is used)
- Env variables: GOOD (Preserves
CALLER and CALLVALUE)
- Memory Safety: Depends on implementation
4. Create special variables which persist across message calls
Built-in vars like caller and callvalue can be created, which are magically passed to a function in a message call.
My code above would thus be:
@private
def checkOwner():
assert _sender == self.owner # <- replaced msg.sender with a magical global variable `sender`
@public
def writeValue(_value: int128):
self.checkOwner)
self.value = _value
This feels a bit funky to me. Probably would be hard to implement safely, and obscures the true functioning of the EVM.
5. Status Quo
My problem in the code sample above can be addressed by passing the values I need as arguments:
@private
def checkOwner(_sender: address):
assert _sender == self.owner # <- replaced msg.sender with _sender
@public
def writeValue(_value: int128):
self.checkOwner(msg.sender) # <- pass msg.sender as an argument
self.value = _value
Maybe that's OK? But it requires a deeper understanding of how both the EVM and Vyper work.
Backwards Compatibility
Dependent on approach.
Copyright
Copyright and related rights waived via CC0
Preamble
Simple Summary
Abstract
In order to call a within the same contract function, Vyper uses the
CALLopcode, which send a new message to call functions within the same contract. This has some nice safety benefits.Unfortunately it will result in developers using anti-patterns to save on gas, or access
msg.senderin a function call.Motivation
The benefit of using
CALLto access code in the same contract is to create a new execution context, with no risk of side effects from memory access. I admit that I find this to be quite an elegant use of the EVM for safety.Problems with using
CALLUnfortunately, there are two major issues with the use of the
CALLopcode for calling functions within the contract:1. Environment values change unexpectedly
At least two important environment opcodes will return different values:
CALLER(ie.msg.sender) andCALLVALUE(ie.msg.value). This complicates the use of functions for permission checking. The following is a naive vyper translation of the common "ownable" pattern for a simple storage solidity contract.2. Gas costs disincentivizes code reuse
The
CALLopcode costs 700 gas (+ input data costs + function dispatching again). By contrast Solidity usesJUMP(2 gas) to call a function.This imposes a large penalty on code reuse. Regardless Vyper's design philosophy favoring safety over gas efficiency, developers will respond to this incentive by simply copying and pasting boilerplate code. This is dangerous, and difficult to audit.
Specification:
I'm sorry, I don't have one specification. Below are some options I see for mitigating these issues.
From the Motivation section above, we have 3 parameters along which to analyze approaches to calling a function in the EVM.
1. Use
DELEGATECALLinstead ofCALLCALLERandCALLVALUE)Seems like a decent solution!
3. Memory Safety: BAD (executes the code in the same memory context, nullifying reasons for using~I proposed this earlier, but I don't think this is a good solution. ~CALLin the first place)2. Just use
JUMPlike SolidityCALLERandCALLVALUE)This is worth considering.
See also #330.
3. Implement a safer analog to Solidity's modifiers
These could have significant restrictions on them, like no access to
MSTOREorSSTORE, and not accepting arguments. I assume this would be done by withJUMP, or just inlining the same code each time it's needed.CALLERandCALLVALUE)4. Create special variables which persist across message calls
Built-in vars like
callerandcallvaluecan be created, which are magically passed to a function in a message call.My code above would thus be:
This feels a bit funky to me. Probably would be hard to implement safely, and obscures the true functioning of the EVM.
5. Status Quo
My problem in the code sample above can be addressed by passing the values I need as arguments:
Maybe that's OK? But it requires a deeper understanding of how both the EVM and Vyper work.
Backwards Compatibility
Dependent on approach.
Copyright
Copyright and related rights waived via CC0