From 07ad0613013c179350c7c87674010794b5a8069a Mon Sep 17 00:00:00 2001 From: Noah Thorp Date: Tue, 2 Feb 2021 23:05:01 -0800 Subject: [PATCH 1/5] More transfer restrictions tests and README changes. --- README.md | 8 ++-- tests/transfer_restrictions.test.js | 63 +++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 90d60c9..c971dc3 100644 --- a/README.md +++ b/README.md @@ -122,9 +122,9 @@ There are lots of other reasons you may get a bad request error, such as TEAL ex There are a few interrelated account reference quirks to keep in mind: * `Txn.accounts[0]` will always evaluate to `Txn.sender()` * `Txn.accounts[1]` is the first `--app-account` item. -* If no `--app-account` items are included, Txn.accounts.length() will be 0 but `Txn.accounts[0]` still resolves to the sender. -* `Txn.accounts[n]` for n > 0 will evaluate to the element at the n-1th index of the --app-account/ForeignAccounts transaction field. -* Some versions of `tealdbg` show the Txn.Accounts array incorrectly. If n accounts are present in the transaction’s ForeignAccounts array, the debugger will show the sender’s account following by the first n-1 elements from ForeignAccounts. +* If no `--app-account` items are included, `Txn.accounts.length()` will be 0 but `Txn.accounts[0]` still resolves to the sender. +* `Txn.accounts[n]` for n > 0 will evaluate to the element at the n-1th index of the `--app-account` or `ForeignAccounts` transaction field. For example `Txn.accounts[2]` would refer to `appAccount[1]`. Another way to put it is the `--from` address is shifted into Txn.accounts[0] and `--app-accounts` are shifted right by 1 position. +* Some versions of `tealdbg` show the `Txn.accounts` array incorrectly. If n accounts are present in the transaction’s `ForeignAccounts` array, the debugger will show the sender’s account following by the first n-1 elements from `ForeignAccounts`. ## Teal contract size @@ -208,7 +208,7 @@ It is recommended that all admin actions should be performed by accounts other t ## QSP-8 Do Algorand Smart Contracts Lack A Standard Like the Ethereum ERC20 Token? -Yes, to accommodate this we use functionality with the same function names and behavior as the Ethereum ERC20 token standard - with the notable exception that the contract does not implement the `approve()` and `transferFrom` functions. See next question... +Yes, to accommodate this we use functionality with the same function names and behavior as the Ethereum ERC20 token standard - with the notable exception that the contract does not implement the `approve()` and `transferFrom()` functions. See next question... ## QSP-9 Why doesn't the contract implement the approve() and transferFrom() functions from the ERC20 standard? diff --git a/tests/transfer_restrictions.test.js b/tests/transfer_restrictions.test.js index 6b409e3..27688f0 100644 --- a/tests/transfer_restrictions.test.js +++ b/tests/transfer_restrictions.test.js @@ -128,20 +128,25 @@ test('can transfer to an account if the transfer rule lock has expired', async ( expect(localState["balance"]["ui"]).toEqual(11) }) -test('cannot transfer by default', async () => { +test('cannot transfer by default from and to the default group 1', async () => { try { appArgs = [EncodeBytes("transfer"), EncodeUint('11')] await util.appCall(clientV2, adminAccount, appId, appArgs, [receiverAccount.addr]) } catch (e) { expect(e.message).toEqual("Bad Request") } - // check first receiver got tokens - localState = await util.readLocalState(clientV2, receiverAccount, appId) + // check first receiver got no tokens and is in group 1 + let localState = await util.readLocalState(clientV2, receiverAccount, appId) + expect(localState["transferGroup"]["ui"]).toEqual(1) expect(localState["balance"]["ui"]).toEqual(undefined) + + // check sender sent no tokens and is in group 1 + localState = await util.readLocalState(clientV2, adminAccount, appId) + expect(localState["transferGroup"]["ui"]).toEqual(1) + expect(localState["balance"]["ui"]).toEqual(27) }) test('can transfer between permitted account groups', async () => { - let earliestPermittedTime = 1 // from group 1 -> 1 is allowed let transferGroupLock1 = @@ -191,4 +196,54 @@ test('can transfer between permitted account groups', async () => { // first account no longer has the transferred tokens localState = await util.readLocalState(clientV2, receiverAccount, appId) expect(localState["balance"]["ui"]).toEqual(4) +}) + +test('transferRule allowing transfer from group 1 to 2 does not allow transfers from 2 to 1 (the reverse rule)', async () => { + let earliestPermittedTime = 1 + + // from group 1 -> 2 is allowed + let transferGroupLock2 = + `goal app call --app-id ${appId} --from ${adminAccount.addr} ` + + `--app-arg 'str:setTransferRule' ` + + `--app-arg "int:1" --app-arg "int:2" ` + + `--app-arg "int:${earliestPermittedTime}" -d devnet/Primary` + + await shell.exec(transferGroupLock2, {async: false, silent: false}) + + // put receiver in group 2 + appArgs = [EncodeBytes("setAddressPermissions"), EncodeUint('0'), EncodeUint('0'), EncodeUint('0'), EncodeUint('2')] + await util.appCall(clientV2, adminAccount, appId, appArgs, [receiverAccount.addr]) + + let localState = await util.readLocalState(clientV2, receiverAccount, appId) + expect(localState["balance"]["ui"]).toEqual(undefined) + expect(localState["transferGroup"]["ui"].toString()).toEqual('2') + + //transfer to receiver (group 1 -> 2) + appArgs = [EncodeBytes("transfer"), EncodeUint('7')] + await util.appCall(clientV2, adminAccount, appId, appArgs, [receiverAccount.addr]) + + // check receiver got tokens + localState = await util.readLocalState(clientV2, receiverAccount, appId) + expect(localState["balance"]["ui"]).toEqual(7) + + // first sender adminAccount no longer has the transferred tokens + localState = await util.readLocalState(clientV2, adminAccount, appId) + expect(localState["balance"]["ui"]).toEqual(20) + + // transfer back from receiver account (group 2 -> 1) FAILS! + let error = null + try { + appArgs = [EncodeBytes("transfer"), EncodeUint('7')] + await util.appCall(clientV2, receiverAccount, appId, appArgs, [adminAccount.addr]) + } catch(e) { + error = e + } + expect(error.message).toBe("Bad Request") + + //balances remain unchanged before and after the failed transfer + localState = await util.readLocalState(clientV2, receiverAccount, appId) + expect(localState["balance"]["ui"]).toEqual(7) + + localState = await util.readLocalState(clientV2, adminAccount, appId) + expect(localState["balance"]["ui"]).toEqual(20) }) \ No newline at end of file From eea93f3c033ded267f318c5ad7bebe58a244c83b Mon Sep 17 00:00:00 2001 From: Noah Thorp Date: Tue, 2 Feb 2021 23:12:10 -0800 Subject: [PATCH 2/5] Test that maxBalance = 0 is treated as no max balance restriction --- tests/max_token_balance.test.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/max_token_balance.test.js b/tests/max_token_balance.test.js index 78707da..aba4e8d 100644 --- a/tests/max_token_balance.test.js +++ b/tests/max_token_balance.test.js @@ -69,6 +69,28 @@ test('blocks transfers that exceed the addresses maxBalance but not lesser amoun appArgs = [EncodeBytes("transfer"), EncodeUint('10')] await util.appCall(clientV2, receiverAccount, appId, appArgs, [adminAccount.addr]) + // tokens sent back to admin + localState = await util.readLocalState(clientV2, receiverAccount, appId) + expect(localState["balance"]["ui"]).toEqual(undefined) +}) + +test('maxBalance of 0 is treated as no max balance', async () => { + let maxTokenBalance = 0 + appArgs = [EncodeBytes("setAddressPermissions"), EncodeUint('0'), EncodeUint(`${maxTokenBalance}`), EncodeUint('0'), EncodeUint('1')] + await util.appCall(clientV2, adminAccount, appId, appArgs, [receiverAccount.addr]) + + // allow token transfers to address with 0 maxBalance + appArgs = [EncodeBytes("transfer"), EncodeUint('10')] + await util.appCall(clientV2, adminAccount, appId, appArgs, [receiverAccount.addr]) + + // tokens sent to receiver + localState = await util.readLocalState(clientV2, receiverAccount, appId) + expect(localState["balance"]["ui"]).toEqual(10) + + // allow tokens to be transferred out of the account + appArgs = [EncodeBytes("transfer"), EncodeUint('10')] + await util.appCall(clientV2, receiverAccount, appId, appArgs, [adminAccount.addr]) + // tokens sent back to admin localState = await util.readLocalState(clientV2, receiverAccount, appId) expect(localState["balance"]["ui"]).toEqual(undefined) From caca0ac24c5b8b41ff7ee4aa5015592c785ed6e4 Mon Sep 17 00:00:00 2001 From: Noah Thorp Date: Tue, 2 Feb 2021 23:13:51 -0800 Subject: [PATCH 3/5] Back to just running CI on push. Otherwise it double runs tests in PRs which is the most common use case. --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9ab3c7f..d052944 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,6 @@ name: Tests -on: [push, pull_request] +on: [push] jobs: test: From 3cd0e27f0a499d69eb1e0ba064741baed9a42602 Mon Sep 17 00:00:00 2001 From: Noah Thorp Date: Tue, 2 Feb 2021 23:34:19 -0800 Subject: [PATCH 4/5] Test that transferRules for transferGroup 0 work fine. --- tests/transfer_restrictions.test.js | 114 +++++++++++++++++++++++----- 1 file changed, 95 insertions(+), 19 deletions(-) diff --git a/tests/transfer_restrictions.test.js b/tests/transfer_restrictions.test.js index 27688f0..c4ef802 100644 --- a/tests/transfer_restrictions.test.js +++ b/tests/transfer_restrictions.test.js @@ -48,7 +48,25 @@ test('has expected starting test state', async () => { expect(localState["transfer admin"]).toEqual(undefined) }) -test('simple transfer', async () => { +test('cannot transfer by default from and to the default group 1 -> 1', async () => { + try { + appArgs = [EncodeBytes("transfer"), EncodeUint('11')] + await util.appCall(clientV2, adminAccount, appId, appArgs, [receiverAccount.addr]) + } catch (e) { + expect(e.message).toEqual("Bad Request") + } + // check first receiver got no tokens and is in group 1 + let localState = await util.readLocalState(clientV2, receiverAccount, appId) + expect(localState["transferGroup"]["ui"]).toEqual(1) + expect(localState["balance"]["ui"]).toEqual(undefined) + + // check sender sent no tokens and is in group 1 + localState = await util.readLocalState(clientV2, adminAccount, appId) + expect(localState["transferGroup"]["ui"]).toEqual(1) + expect(localState["balance"]["ui"]).toEqual(27) +}) + +test('simple transfer back and forth: with group 1 -> 1 permitted', async () => { let fromGroupId = 1 let toGroupId = 1 let earliestPermittedTime = 1 @@ -80,6 +98,24 @@ test('simple transfer', async () => { globalState = await util.readGlobalState(clientV2, adminAccount, appId) expect(globalState['cap']['ui'].toString()).toEqual('80000000000000000') expect(globalState['reserve']['ui'].toString()).toEqual('79999999999999973') + + // ====== + //transfer back + appArgs = [EncodeBytes("transfer"), EncodeUint('11')] + await util.appCall(clientV2, receiverAccount, appId, appArgs, [adminAccount.addr]) + + // check original sender got tokens back + localState = await util.readLocalState(clientV2, adminAccount, appId) + expect(localState["balance"]["ui"]).toEqual(27) + + // check tokens deducted + localState = await util.readLocalState(clientV2, receiverAccount, appId) + expect(localState["balance"]["ui"]).toEqual(undefined) + + // check global supply is same + globalState = await util.readGlobalState(clientV2, adminAccount, appId) + expect(globalState['cap']['ui'].toString()).toEqual('80000000000000000') + expect(globalState['reserve']['ui'].toString()).toEqual('79999999999999973') }) test('can lock the default address category for transfers', async () => { @@ -107,6 +143,64 @@ test('can lock the default address category for transfers', async () => { expect(localState["balance"]["ui"]).toEqual(undefined) }) +test('simple transfer from group 0 -> 0 works when permitted', async () => { + let fromGroupId = 0 + let toGroupId = 0 + let earliestPermittedTime = 1 + + let transferGroupLock = + `goal app call --app-id ${appId} --from ${adminAccount.addr} ` + + `--app-arg 'str:setTransferRule' ` + + `--app-arg "int:${fromGroupId}" --app-arg "int:${toGroupId}" ` + + `--app-arg "int:${earliestPermittedTime}" -d devnet/Primary` + + appArgs = [EncodeBytes("setAddressPermissions"), EncodeUint('0'), EncodeUint('0'), EncodeUint('0'), EncodeUint('0')] + await util.appCall(clientV2, adminAccount, appId, appArgs, [adminAccount.addr]) + await util.appCall(clientV2, adminAccount, appId, appArgs, [receiverAccount.addr]) + + await shell.exec(transferGroupLock, {async: false, silent: false}) + + globalState = await util.readGlobalState(clientV2, adminAccount, appId) + expect(globalState['reserve']['ui'].toString()).toEqual('79999999999999973') + + //transfer + appArgs = [EncodeBytes("transfer"), EncodeUint('11')] + await util.appCall(clientV2, adminAccount, appId, appArgs, [receiverAccount.addr]) + + // check receiver got tokens + localState = await util.readLocalState(clientV2, receiverAccount, appId) + expect(localState["balance"]["ui"]).toEqual(11) + expect(localState["transferGroup"]["ui"]).toEqual(undefined) + + // check sender has less tokens + localState = await util.readLocalState(clientV2, adminAccount, appId) + expect(localState["balance"]["ui"]).toEqual(16) + expect(localState["transferGroup"]["ui"]).toEqual(undefined) + + // check global supply is same + globalState = await util.readGlobalState(clientV2, adminAccount, appId) + expect(globalState['cap']['ui'].toString()).toEqual('80000000000000000') + expect(globalState['reserve']['ui'].toString()).toEqual('79999999999999973') + + // ====== + //transfer back + appArgs = [EncodeBytes("transfer"), EncodeUint('11')] + await util.appCall(clientV2, receiverAccount, appId, appArgs, [adminAccount.addr]) + + // check original sender got tokens back + localState = await util.readLocalState(clientV2, adminAccount, appId) + expect(localState["balance"]["ui"]).toEqual(27) + + // check tokens deducted + localState = await util.readLocalState(clientV2, receiverAccount, appId) + expect(localState["balance"]["ui"]).toEqual(undefined) + + // check global supply is same + globalState = await util.readGlobalState(clientV2, adminAccount, appId) + expect(globalState['cap']['ui'].toString()).toEqual('80000000000000000') + expect(globalState['reserve']['ui'].toString()).toEqual('79999999999999973') +}) + test('can transfer to an account if the transfer rule lock has expired', async () => { let fromGroupId = 1 let toGroupId = 1 @@ -128,24 +222,6 @@ test('can transfer to an account if the transfer rule lock has expired', async ( expect(localState["balance"]["ui"]).toEqual(11) }) -test('cannot transfer by default from and to the default group 1', async () => { - try { - appArgs = [EncodeBytes("transfer"), EncodeUint('11')] - await util.appCall(clientV2, adminAccount, appId, appArgs, [receiverAccount.addr]) - } catch (e) { - expect(e.message).toEqual("Bad Request") - } - // check first receiver got no tokens and is in group 1 - let localState = await util.readLocalState(clientV2, receiverAccount, appId) - expect(localState["transferGroup"]["ui"]).toEqual(1) - expect(localState["balance"]["ui"]).toEqual(undefined) - - // check sender sent no tokens and is in group 1 - localState = await util.readLocalState(clientV2, adminAccount, appId) - expect(localState["transferGroup"]["ui"]).toEqual(1) - expect(localState["balance"]["ui"]).toEqual(27) -}) - test('can transfer between permitted account groups', async () => { let earliestPermittedTime = 1 // from group 1 -> 1 is allowed From 03b2db987c24398944701b4e38054911d24950d1 Mon Sep 17 00:00:00 2001 From: Noah Thorp Date: Tue, 2 Feb 2021 23:57:00 -0800 Subject: [PATCH 5/5] Refactor grant roles test --- tests/grant_roles.test.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/grant_roles.test.js b/tests/grant_roles.test.js index eb70f4c..0b72b53 100644 --- a/tests/grant_roles.test.js +++ b/tests/grant_roles.test.js @@ -8,7 +8,7 @@ const { describe } = require('yargs') const server = "http://127.0.0.1" const port = 8080 -var adminAccount, receiverAccount, token, clientV2, appId +var adminAccount, receiverAccount, token, clientV2, appId, localState beforeEach(async () => { await privateTestNetSetup(appId) @@ -23,17 +23,23 @@ beforeEach(async () => { await util.optInApp(clientV2, receiverAccount, appId) }) -test('contract admin role can be granted by contract admin', async () => { +async function grantRoles(roleId, from=adminAccount, target=receiverAccount) { appArgs = [ EncodeBytes("grantRoles"), - EncodeUint('8') + EncodeUint(roleId) ] - await util.appCall(clientV2, adminAccount, appId, appArgs, [receiverAccount.addr]) + await util.appCall(clientV2, from, appId, appArgs, [target.addr]) +} + +test('contract admin role can be granted by contract admin', async () => { + await grantRoles(8, adminAccount, receiverAccount) localState = await util.readLocalState(clientV2, receiverAccount, appId) expect(localState["roles"]["ui"]).toEqual(8) - await util.appCall(clientV2, receiverAccount, appId, appArgs, [receiverAccount.addr]) + await grantRoles(15, receiverAccount, receiverAccount) + localState = await util.readLocalState(clientV2, receiverAccount, appId) + expect(localState["roles"]["ui"]).toEqual(15) }) test('contract admin role can be revoked by contract admin', async () => {