factorial() { local N; eval let N=1 N*={1..$1}; echo "$N"; }
Using let, this takes advantage of its mathematical functions and argument-based variable assignments to serially overwrite the last known value of N with the product of N and the next integer generated by the expansion of {1..$1} into a list of incremental numbers.
Since the sequence expression {x..y} only works with literal values, {1..$1} would not be expanded to a sequence of numbers. For example when $1 is 3, then {1..$1} expands to {1..3} instead of the sequence 1 2 3. This is the reason we use eval, so that in this example evaluating the expression {1..3} expands to the sequence 1 2 3.
The final expression executed by the shell after all expansions are applied is equivalent to:
let N=1 N=N*1 N=N*2 N=N*3 ... N=N*$1
factorial() ( IFS=\*; let N=$1-1 k="$*" && factorial "$N" "$k" || echo ${2-1} )