diff --git a/arc-0020/.gitignore b/arc-0020/.gitignore new file mode 100644 index 0000000..0a2a8d9 --- /dev/null +++ b/arc-0020/.gitignore @@ -0,0 +1 @@ +build_** diff --git a/arc-0020/README.md b/arc-0020/README.md new file mode 100644 index 0000000..36f4155 --- /dev/null +++ b/arc-0020/README.md @@ -0,0 +1,45 @@ +--- +arc: 20 +title: Design for ARC20 +authors: The Aleo Team +discussion: https://github.com/AleoHQ/ARCs/discussions/42 +topic: Application +status: Living +created: 2023-10-31 +--- + +*A big thank you to [Valentin Seehausen](https://github.com/Valentin-Seehausen), [Evan Marshall](https://github.com/evanmarshall), [FullTimeMike](https://github.com/fulltimemike) and authors of the previous two ARC20 specs [Ghostant-1017](https://github.com/ghostant-1017) and [EdVeralli](https://github.com/EdVeralli).* + +## Abstract + +This ARC introduces a design for minimal Fungible Tokens just like ERC20. It allows for transferring tokens and approving programs to transfer tokens on your behalf. + +Given that Aleo does not support interfaces or inheritance, this spec is not enforced by the compiler. However, we invite the community to adhere to these standards in an effort to enhance interoperability between programs. + +The previous two ARC20 standards were built on old versions of snarkVM. Now that functionality is stabilizing, we can integrate the community's learnings in a new standard. This minimal initial standard is written in Aleo instructions for simplicity and enhanced auditability. In the future there are many extensions which might be valuable to standardize: +- ERC1155-like multi-token standard +- minting functionality +- multisig or admin functionality + +Notes: +- One can approve for more than the existing balance, but spending approved funds from others is of course limited by their balance. +- No metadata is added to the program spec, though deployed program names can suffice as globally unique identifiers. +- An update to snarkVM should soon enable passing an aleo program id as program input to `snarkos developer execute` +- Before committing, this should be audited against the `credits.aleo` program. + +## Specification + +[token.aleo](./token.aleo) + +## Testing + +You can test this program on a local devnet. First, set up the devnet. Development private keys and addresses are printed to the terminal. Because the devnet runs in tmux, You can scroll up using `ctrl+b+[`. Be quick because history is limited by default. + +``` +git clone github.com/aleoHQ/snarkOS +cd snarkOS +git checkout ca3e84c48 +./devnet.sh +``` + +Then run the `test.sh` script from the folder in this ARC repository. diff --git a/arc-0020/spender_tester.aleo b/arc-0020/spender_tester.aleo new file mode 100644 index 0000000..6e978b7 --- /dev/null +++ b/arc-0020/spender_tester.aleo @@ -0,0 +1,16 @@ +import token.aleo; + +program spender_tester.aleo; + +function transfer_from_public: + input r0 as address.public; // approver + input r1 as address.public; // receiver + input r2 as u64.public; + + call token.aleo/transfer_from_public r0 r1 r2 into r3; + async transfer_from_public r3 into r4; + output r4 as spender_tester.aleo/transfer_from_public.future; + +finalize transfer_from_public: + input r0 as token.aleo/transfer_from_public.future; + await r0; diff --git a/arc-0020/spender_tester_program.json b/arc-0020/spender_tester_program.json new file mode 100644 index 0000000..460a8a8 --- /dev/null +++ b/arc-0020/spender_tester_program.json @@ -0,0 +1,6 @@ +{ + "program": "spender_tester.aleo", + "version": "0.0.0", + "description": "", + "license": "MIT" +} diff --git a/arc-0020/test.sh b/arc-0020/test.sh new file mode 100755 index 0000000..bf29387 --- /dev/null +++ b/arc-0020/test.sh @@ -0,0 +1,60 @@ +set -e + +# Read the approver private key from the user +read -p "Enter the private key with positive public account balance: " approver_private_key +# Read the approver addresss from the user +read -p "Enter the associated address with positive public account balance: " approver_address + +# throwaway keys +spender_tester_private_key="APrivateKey1zkpB6TurrGgShJ7dsJ21HniMTF5WQc2eRy7d5o5QyBZFMFf" +spender_tester_address="aleo1ew25qvyvd33gk9w4m8ehyhjamsm74r7n3ze9npnpux0rntzajgpqcm26pz" + + +mkdir -p build_token +cp token.aleo build_token/main.aleo +cp token_program.json build_token/program.json + +echo """ + +/* This is only used for testing the spec */ +function mint_public: + input r0 as address.public; + input r1 as u64.public; + async mint_public r0 r1 into r2; + output r2 as token.aleo/mint_public.future; + +finalize mint_public: + input r0 as address.public; + input r1 as u64.public; + get.or_use account[r0] 0u64 into r2; + add r2 r1 into r3; + set r3 into account[r0]; +""" >> build_token/main.aleo + +mkdir -p build_spender_tester +cp spender_tester.aleo build_spender_tester/main.aleo +cp spender_tester_program.json build_spender_tester/program.json +mkdir -p build_spender_tester/imports +cp build_token/main.aleo build_spender_tester/imports/token.aleo + +# deploy +snarkos developer deploy token.aleo --private-key ${approver_private_key} --query "http://localhost:3030" --path "build_token" --broadcast "http://localhost:3030/testnet3/transaction/broadcast" --priority-fee 0 + +snarkos developer deploy spender_tester.aleo --private-key ${approver_private_key} --query "http://localhost:3030" --path "build_spender_tester" --broadcast "http://localhost:3030/testnet3/transaction/broadcast" --priority-fee 0 + +echo letting deployments settle for a few seconds... +sleep 10 + +# mint tokens +snarkos developer execute token.aleo mint_public ${approver_address} 10u64 --private-key ${approver_private_key} --query "http://localhost:3030" --broadcast "http://localhost:3030/testnet3/transaction/broadcast" + +# Transfer to spender so they have enough to cover the fee +snarkos developer execute credits.aleo transfer_public ${spender_tester_address} 100000u64 --private-key ${approver_private_key} --query "http://localhost:3030" --broadcast "http://localhost:3030/testnet3/transaction/broadcast" + +snarkos developer execute token.aleo approve_public ${spender_tester_address} 1u64 --private-key ${approver_private_key} --query "http://localhost:3030" --broadcast "http://localhost:3030/testnet3/transaction/broadcast" + +snarkos developer execute token.aleo transfer_from_public ${approver_address} ${spender_tester_address} 1u64 --private-key ${spender_tester_private_key} --query "http://localhost:3030" --broadcast "http://localhost:3030/testnet3/transaction/broadcast" + +snarkos developer execute token.aleo approve_public big_spender.aleo 1u64 --private-key ${approver_private_key} --query "http://localhost:3030" --broadcast "http://localhost:3030/testnet3/transaction/broadcast" + +snarkos developer execute token.aleo transfer_from_public big_spender.aleo ${spender_tester_address} 1u64 --private-key ${spender_tester_private_key} --query "http://localhost:3030" --broadcast "http://localhost:3030/testnet3/transaction/broadcast" diff --git a/arc-0020/token.aleo b/arc-0020/token.aleo new file mode 100644 index 0000000..57f812c --- /dev/null +++ b/arc-0020/token.aleo @@ -0,0 +1,148 @@ +program token.aleo; + +mapping account: + key as address.public; + value as u64.public; + +record token: + owner as address.private; + amount as u64.private; + +struct approval: + approver as address; + spender as address; + +mapping approvals: + key as field.public; + value as u64.public; + +function approve_public: + input r0 as address.public; // spender + input r1 as u64.public; // amount spender is allowed to withdraw from approver + + // hash approval + cast self.caller r0 into r2 as approval; + hash.bhp256 r2 into r3 as field; + + async approve_public r3 r1 into r4; + output r4 as token.aleo/approve_public.future; + +finalize approve_public: + input r0 as field.public; + input r1 as u64.public; // increase in amount spender is allowed to withdraw from approver + + // if approvals for approval field exists, the approved amount is increased. + // otherwise, the approved allowance is created. + get.or_use approvals[r0] 0u64 into r2; + add r1 r2 into r3; + set r3 into approvals[r0]; + +function unapprove_public: + input r0 as address.public; // spender + input r1 as u64.public; // amount spender's allowance is decreasing by + + // hash approval + cast self.caller r0 into r2 as approval; + hash.bhp256 r2 into r3 as field; + + async unapprove_public r3 r1 into r4; + output r4 as token.aleo/unapprove_public.future; + +finalize unapprove_public: + input r0 as field.public; + input r1 as u64.public; // decrease in amount spender is allowed to withdraw from approver + + get approvals[r0] into r2; + sub r2 r1 into r3; + set r3 into approvals[r0]; + +/* Transfer From */ + +function transfer_from_public: + input r0 as address.public; // from the approver + input r1 as address.public; // to the receiver + input r2 as u64.public; // amount to transfer + + cast r0 self.caller into r3 as approval; + hash.bhp256 r3 into r4 as field; // hash approval + + async transfer_from_public r4 r0 r1 r2 into r5; + output r5 as token.aleo/transfer_from_public.future; + +finalize transfer_from_public: + input r0 as field.public; // approval + input r1 as address.public; // from the approver + input r2 as address.public; // to the receiver + input r3 as u64.public; // amount to transfer + + get approvals[r0] into r4; + sub r4 r3 into r5; + set r5 into approvals[r0]; + get account[r1] into r6; + sub r6 r3 into r7; + set r7 into account[r1]; + get.or_use account[r2] 0u64 into r8; + add r8 r3 into r9; + set r9 into account[r2]; + +function transfer_public: + input r0 as address.public; + input r1 as u64.public; + async transfer_public self.caller r0 r1 into r2; + output r2 as token.aleo/transfer_public.future; + +finalize transfer_public: + input r0 as address.public; + input r1 as address.public; + input r2 as u64.public; + get.or_use account[r0] 0u64 into r3; + sub r3 r2 into r4; + set r4 into account[r0]; + get.or_use account[r1] 0u64 into r5; + add r5 r2 into r6; + set r6 into account[r1]; + + +function transfer_private: + input r0 as token.record; + input r1 as address.private; + input r2 as u64.private; + sub r0.amount r2 into r3; + cast r0.owner r3 into r4 as token.record; + cast r1 r2 into r5 as token.record; + output r4 as token.record; + output r5 as token.record; + + +function transfer_private_to_public: + input r0 as token.record; + input r1 as address.public; + input r2 as u64.public; + sub r0.amount r2 into r3; + cast r0.owner r3 into r4 as token.record; + async transfer_private_to_public r1 r2 into r5; + output r4 as token.record; + output r5 as token.aleo/transfer_private_to_public.future; + +finalize transfer_private_to_public: + input r0 as address.public; + input r1 as u64.public; + get.or_use account[r0] 0u64 into r2; + add r2 r1 into r3; + set r3 into account[r0]; + + +function transfer_public_to_private: + input r0 as address.public; + input r1 as u64.public; + cast r0 r1 into r2 as token.record; + async transfer_public_to_private self.caller r1 into r3; + output r2 as token.record; + output r3 as token.aleo/transfer_public_to_private.future; + +finalize transfer_public_to_private: + input r0 as address.public; + input r1 as u64.public; + get.or_use account[r0] 0u64 into r2; + sub r2 r1 into r3; + set r3 into account[r0]; diff --git a/arc-0020/token_program.json b/arc-0020/token_program.json new file mode 100644 index 0000000..94d9bc9 --- /dev/null +++ b/arc-0020/token_program.json @@ -0,0 +1,6 @@ +{ + "program": "token.aleo", + "version": "0.0.0", + "description": "", + "license": "MIT" +}