Assuming you are doing C style calls and C style access then pretty much this. There are some optimizations though.
For the entry
PUSH IY
LD SP, -n ; where n allows for the size of the local variable frame
ADD IY,SP ; IY now points at the bottom of the new frame including locals
LD SP,IY ; adjust the stack
On the return path you can do various things to adjust the stack - for small values repeatedly popping af is typically smallest and fastest if not you end up doing something like, although you want to use HL if you can (depending what values you need to pass back and how you can pass them back)
LD IY,-n
ADD IY,SP
LD SP,IY
POP IY
RET (no point doing POP and JMP (IX) )
All the (IY+n) stuff though is 8bit and *really* slow so you want to keep everything in registers as much as possible. In many 16boit cases with more values than registers it's better to load and push the contents of memory locations on entry, use the fixed memory locations for calculation and then restore the old contents of the memory location on exit, than pay the hideous cost of Z80 indexed ops. On 8080 you really don't have much choice either.
For real CP/M programs (8080 code) you don't have any really easy way to do this. A lot of the compilers don't use frame pointers at all but instead use helpers that do
LD HL,offset
ADD HL,SP
LD E,(HL)
INC HL
LD D,(HL)
RET
or similar which is actually faster in many cases anyway (particularly when inlined)
FCC on Z80 uses all three methods depending upon code size and register usage. On 8080 it uses either inline or call helpers for the ADD HL,SP approach.
SDCC (and the version in Z88DK) does all sorts of magic and register usage analysis to try and keep stuff in registers instead, but as a result it won't run on a Z80 box only a modern PC.
If you are working in assembler then you can be a lot smarter than most compilers as you know the exact constraints. When you try and get fast recursive code on 8080 or Z80 you normally end up doing more like
func: PUSH BC ; save old working values if needed, also use as parameters
PUSH HL
LD HL,(tmp) ; save working values we use recursively onto stack
PUSH HL ; etc for each
; Do stuff in BC/DE/HL result in DE and use tmp as if it was private
CALL func ; as needed
; whatever
POP HL
LD (tmp),HL ; put old working values back
POP HL
POP BC
RET
and it is commonly the case that it's much better to switch to a non-recursive version of the algorithm and use the stack purely as a data stack.
You'll also find that for 8080 compatible code it's often better to return a value in DE, whereas for Z80 HL is often more useful. That's partly because you've got no IX/IY to borrow for stack adjustment and partly because 8080 has no LD DE,(nnnn) or LD (nnnn),DE forms so you on 8080 you end up using XTHL SHLD nnnn XTHL stuff if you use HL for return values - but that varies sometimes.
Finally for Z80 there's a useful trick for single level recursion where you know it's only one level (eg SD card commands and extended commands) which is to use EXX and the alt registers for the recursive call as EXX is incredibly fast and nobody ever uses the alt registers for much 8)