NOTE: This answer uses eval, and should be avoided where possible. See Freddy's answer, which uses declare -n / local -n to create a named reference.
This approach may be necessary if you're using an old version of bash.
As already pointed out, you're passing the literal string arr, and that isn't an array, hence the "bad substitution".
You can to use eval to build a string that results in expanding to what you're after:
arr_to_file() {
local cmd
printf -v cmd 'local -a _content=( "${%q[@]}" )' "${1}"
eval "${cmd}"
printf '%s\n' "${_content[@]}" > "${2}"
}
mapfile -t arr < <(echo "one" ; echo "two" ; echo "three")
arr_to_file arr file
eval usually gets a big warning notice, so here's yours: "eval is super dangerous if you don't trust the string you're giving it".
If we're very careful with eval, then it can be used somewhat safely. Here printf "%q" "${1}" is used to quote the first argument suitably for shell input (e.g: printf '%q' '}' won't break out of the variable's name). Thanks to Charles Duffy for the revised and significantly safer and more readable snippet.
This approach will also work for associative arrays (i.e: arrays that use keys instead of indexes), but the output will be unordered.