From ed4f37907071651bbb1d58232f785302b067e10b Mon Sep 17 00:00:00 2001 From: Satyam Agrawal Date: Wed, 22 Mar 2023 14:13:44 +0530 Subject: [PATCH] create new branch as #125 get corrupted --- contracts/FungibleTokenSwitchboard.cdc | 31 +++++++ contracts/utility/TokenForwarding.cdc | 33 +++++++- flow.json | 3 +- lib/js/test/core_features.test.js | 2 +- .../transactions/fix_receiver_linking.cdc | 18 ++++ .../setup_infinite_loop_capability.cdc | 18 ++++ lib/js/test/switchboard.test.js | 82 ++++++++++++++++++- transactions/create_forwarder.cdc | 6 ++ .../switchboard/check_receiver_by_type.cdc | 10 +++ .../tokenForwarder/is_recipient_valid.cdc | 10 +++ .../switchboard/safe_transfer_tokens_v2.cdc | 45 ++++++++++ 11 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 lib/js/test/mocks/transactions/fix_receiver_linking.cdc create mode 100644 lib/js/test/mocks/transactions/setup_infinite_loop_capability.cdc create mode 100644 transactions/scripts/switchboard/check_receiver_by_type.cdc create mode 100644 transactions/scripts/tokenForwarder/is_recipient_valid.cdc create mode 100644 transactions/switchboard/safe_transfer_tokens_v2.cdc diff --git a/contracts/FungibleTokenSwitchboard.cdc b/contracts/FungibleTokenSwitchboard.cdc index 493cc56d..39f74c34 100644 --- a/contracts/FungibleTokenSwitchboard.cdc +++ b/contracts/FungibleTokenSwitchboard.cdc @@ -40,6 +40,8 @@ pub contract FungibleTokenSwitchboard { pub fun getSupportedVaultTypes(): {Type: Bool} pub fun deposit(from: @FungibleToken.Vault) pub fun safeDeposit(from: @FungibleToken.Vault): @FungibleToken.Vault? + pub fun checkReceiverByType(type: Type): Bool + pub fun safeBorrowByType(type: Type): &{FungibleToken.Receiver}? } /// The resource that stores the multiple fungible token receiver @@ -251,6 +253,35 @@ pub contract FungibleTokenSwitchboard { return nil } + /// Checks that the capability tied to a type is valid + /// + /// @param vaultType: The type of the ft vault whose capability needs to be checked + /// + /// @return a boolean marking the capability for a type as valid or not + pub fun checkReceiverByType(type: Type): Bool { + if self.receiverCapabilities[type] == nil { + return false + } + + return self.receiverCapabilities[type]!.check() + } + + /// Gets the receiver assigned to a provided vault type. + /// This is necessary because without it, it is not possible to look under the hood and see if a capability + /// is of an expected type or not. This helps guard against infinitely chained TokenForwarding or other invalid + /// malicious kinds of updates that could prevent listings from being made that are valid on storefronts. + /// + /// @param vaultType: The type of the ft vault whose capability needs to be checked + /// + /// @return an optional receiver capability for consumers of the switchboard to check/validate on their own + pub fun safeBorrowByType(type: Type): &{FungibleToken.Receiver}? { + if !self.checkReceiverByType(type: type) { + return nil + } + + return self.receiverCapabilities[type]!.borrow() + } + /// A getter function to know which tokens a certain switchboard /// resource is prepared to receive. /// diff --git a/contracts/utility/TokenForwarding.cdc b/contracts/utility/TokenForwarding.cdc index 539cb828..c1d54772 100644 --- a/contracts/utility/TokenForwarding.cdc +++ b/contracts/utility/TokenForwarding.cdc @@ -21,7 +21,22 @@ pub contract TokenForwarding { // Event that is emitted when tokens are deposited to the target receiver pub event ForwardedDeposit(amount: UFix64, from: Address?) - pub resource Forwarder: FungibleToken.Receiver { + pub resource interface ForwarderPublic { + + /// Helper function to check whether set `recipient` capability + /// is not latent or the capability tied to a type is valid. + pub fun check(): Bool + + /// Gets the receiver assigned to a recipient capability. + /// This is necessary because without it, it is not possible to look under the hood and see if a capability + /// is of an expected type or not. This helps guard against infinitely chained TokenForwarding or other invalid + /// malicious kinds of updates that could prevent listings from being made that are valid on storefronts. + /// + /// @return an optional receiver capability for consumers of the TokenForwarding to check/validate on their own + pub fun safeBorrow(): &{FungibleToken.Receiver}? + } + + pub resource Forwarder: FungibleToken.Receiver, ForwarderPublic { // This is where the deposited tokens will be sent. // The type indicates that it is a reference to a receiver @@ -43,6 +58,22 @@ pub contract TokenForwarding { emit ForwardedDeposit(amount: balance, from: self.owner?.address) } + /// Helper function to check whether set `recipient` capability + /// is not latent or the capability tied to a type is valid. + pub fun check(): Bool { + return self.recipient.check<&{FungibleToken.Receiver}>() + } + + /// Gets the receiver assigned to a recipient capability. + /// This is necessary because without it, it is not possible to look under the hood and see if a capability + /// is of an expected type or not. This helps guard against infinitely chained TokenForwarding or other invalid + /// malicious kinds of updates that could prevent listings from being made that are valid on storefronts. + /// + /// @return an optional receiver capability for consumers of the TokenForwarding to check/validate on their own + pub fun safeBorrow(): &{FungibleToken.Receiver}? { + return self.recipient.borrow<&{FungibleToken.Receiver}>() + } + // changeRecipient changes the recipient of the forwarder to the provided recipient // pub fun changeRecipient(_ newRecipient: Capability) { diff --git a/flow.json b/flow.json index a8005c6e..64b49d8c 100644 --- a/flow.json +++ b/flow.json @@ -103,7 +103,8 @@ "NonFungibleToken", "MetadataViews", "FungibleTokenMetadataViews", - "FungibleTokenSwitchboard" + "FungibleTokenSwitchboard", + "TokenForwarding" ] } } diff --git a/lib/js/test/core_features.test.js b/lib/js/test/core_features.test.js index 7b05774e..427df64f 100644 --- a/lib/js/test/core_features.test.js +++ b/lib/js/test/core_features.test.js @@ -41,7 +41,7 @@ const setup_token_forwarding_tx = fs.readFileSync(path.resolve(__dirname, "./moc const setup_demo_token_tx = fs.readFileSync(path.resolve(__dirname, "./mocks/transactions/setup_account_demo.cdc"), {encoding:'utf8', flag:'r'}); const safe_generic_transfer_tx = fs.readFileSync(path.resolve(__dirname, "./mocks/transactions/safe_generic_transfer.cdc"), {encoding:'utf8', flag:'r'}); const get_balance_read = fs.readFileSync(path.resolve(__dirname, "./mocks/transactions/scripts/get_balance.cdc"), {encoding:'utf8', flag:'r'}); - +const is_recipient_valid = fs.readFileSync(path.resolve(__dirname, "./../../../transactions/scripts/tokenForwarder/is_recipient_valid.cdc"), {encoding:'utf8', flag:'r'}); const token_contract_code = fs.readFileSync(path.resolve(__dirname, "./mocks/contracts/Token.cdc"), {encoding:'utf8', flag:'r'}); diff --git a/lib/js/test/mocks/transactions/fix_receiver_linking.cdc b/lib/js/test/mocks/transactions/fix_receiver_linking.cdc new file mode 100644 index 00000000..6449570b --- /dev/null +++ b/lib/js/test/mocks/transactions/fix_receiver_linking.cdc @@ -0,0 +1,18 @@ +import FungibleToken from "../../../../../contracts/FungibleToken.cdc" +import ExampleToken from "../../../../../contracts/ExampleToken.cdc" +import FungibleTokenSwitchboard from "../../../../../contracts/FungibleTokenSwitchboard.cdc" + +transaction() { + + prepare(signer: AuthAccount) { + + signer.unlink(FungibleTokenSwitchboard.ReceiverPublicPath) + + // Create a public capability to the Vault that only exposes + // the deposit function through the Receiver interface + signer.link<&ExampleToken.Vault{FungibleToken.Receiver}>( + ExampleToken.ReceiverPublicPath, + target: ExampleToken.VaultStoragePath + ) + } +} \ No newline at end of file diff --git a/lib/js/test/mocks/transactions/setup_infinite_loop_capability.cdc b/lib/js/test/mocks/transactions/setup_infinite_loop_capability.cdc new file mode 100644 index 00000000..a63ed318 --- /dev/null +++ b/lib/js/test/mocks/transactions/setup_infinite_loop_capability.cdc @@ -0,0 +1,18 @@ +import FungibleToken from "../../../../../contracts/FungibleToken.cdc" +import ExampleToken from "../../../../../contracts/ExampleToken.cdc" +import FungibleTokenSwitchboard from "../../../../../contracts/FungibleTokenSwitchboard.cdc" + +transaction() { + + prepare(signer: AuthAccount) { + + signer.unlink(ExampleToken.ReceiverPublicPath) + + // Create a public capability to the Vault that only exposes + // the deposit function through the Receiver interface + signer.link<&ExampleToken.Vault{FungibleToken.Receiver}>( + FungibleTokenSwitchboard.ReceiverPublicPath, + target: ExampleToken.VaultStoragePath + ) + } +} \ No newline at end of file diff --git a/lib/js/test/switchboard.test.js b/lib/js/test/switchboard.test.js index ad7e6428..dfecf93b 100644 --- a/lib/js/test/switchboard.test.js +++ b/lib/js/test/switchboard.test.js @@ -23,7 +23,10 @@ async function deployContract(param) { const get_supported_vault_types = fs.readFileSync(path.resolve(__dirname, "./../../../transactions/scripts/get_supported_vault_types.cdc"), {encoding:'utf8', flag:'r'}); const get_vault_types = fs.readFileSync(path.resolve(__dirname, "./../../../transactions/scripts/switchboard/get_vault_types.cdc"), {encoding:'utf8', flag:'r'}); +const check_receiver_by_type = fs.readFileSync(path.resolve(__dirname, "./../../../transactions/scripts/switchboard/check_receiver_by_type.cdc"), {encoding:'utf8', flag:'r'}); const get_balance = fs.readFileSync(path.resolve(__dirname, "./../../../transactions/scripts/get_balance.cdc"), {encoding:'utf8', flag:'r'}); +const setup_infinite_loop_capability = fs.readFileSync(path.resolve(__dirname, "./mocks/transactions/setup_infinite_loop_capability.cdc"), {encoding:'utf8', flag:'r'}); +const fix_receiver_linking = fs.readFileSync(path.resolve(__dirname, "./mocks/transactions/fix_receiver_linking.cdc"), {encoding:'utf8', flag:'r'}); // Defining the test suite for the fungible token switchboard describe("fungibletokenswitchboard", ()=>{ @@ -114,7 +117,84 @@ describe("fungibletokenswitchboard", ()=>{ ); }); - // Third test checks if switchboard user is able to remove ft token vault capabilities + // Third test to check whether added capabilities has infinite forwarding loop or not. + test("should be able validate the added capability", async () => { + // First step: setup switchboard + await shallPass( + sendTransaction({ + name: "switchboard/setup_account", + args: [], + signers: [fungibleTokenSwitchboardUser] + }) + ); + //Second step: setup example token vault + await shallPass( + sendTransaction({ + name: "setup_account", + args: [], + signers: [fungibleTokenSwitchboardUser] + }) + ); + + //Third step: add example token vault capability + await shallPass( + sendTransaction({ + name: "switchboard/add_vault_capability", + args: [], + signers: [fungibleTokenSwitchboardUser] + }) + ); + + // Fourth step: setup infinite loop capability. + await shallPass( + sendTransaction({ + code: setup_infinite_loop_capability, + args: [], + signers: [fungibleTokenSwitchboardUser] + }) + ); + + // Execute script to validate that added capability is not valid anymore + const [result, e] = await executeScript({ + code: check_receiver_by_type, + args: [fungibleTokenSwitchboardUser] + }); + expect(result).toStrictEqual(false); + + // Try to fund the capability with funds + await shallPass( + sendTransaction({ + name: "switchboard/safe_transfer_tokens_v2", + args: [fungibleTokenSwitchboardUser, 10.0], + signers: [serviceAccount] + }) + ) + + // Check balance and it should not increase as the transfer would not happen + const [balance, error] = await executeScript({ + code: get_balance, + args: [fungibleTokenSwitchboardUser] + }); + expect(parseFloat(balance)).toBeCloseTo(0); + + // Let's fix the linking and transfer the funds + await shallPass( + sendTransaction({ + code: fix_receiver_linking, + args: [], + signers: [fungibleTokenSwitchboardUser] + }) + ); + + // Execute script to validate that added capability is valid + const [isValid, err] = await executeScript({ + code: check_receiver_by_type, + args: [fungibleTokenSwitchboardUser] + }); + expect(isValid).toStrictEqual(true); + }); + + // Fourth test checks if switchboard user is able to remove ft token vault capabilities test("should be able to create and remove vault capability", async () => { // First step: setup switchboard await shallPass( diff --git a/transactions/create_forwarder.cdc b/transactions/create_forwarder.cdc index ef3a6ecc..9edb4d20 100644 --- a/transactions/create_forwarder.cdc +++ b/transactions/create_forwarder.cdc @@ -49,5 +49,11 @@ transaction(receiver: Address) { ExampleToken.ReceiverPublicPath, target: /storage/exampleTokenForwarder ) + + // Link the new ForwarderPublic capability + acct.link<&{TokenForwarding.ForwarderPublic}>( + /public/exampleTokenForwarder, + target: /storage/exampleTokenForwarder + ) } } \ No newline at end of file diff --git a/transactions/scripts/switchboard/check_receiver_by_type.cdc b/transactions/scripts/switchboard/check_receiver_by_type.cdc new file mode 100644 index 00000000..d8b92f61 --- /dev/null +++ b/transactions/scripts/switchboard/check_receiver_by_type.cdc @@ -0,0 +1,10 @@ +import FungibleTokenSwitchboard from "../../../contracts/FungibleTokenSwitchboard.cdc" +import ExampleToken from "../../../contracts/ExampleToken.cdc" + +pub fun main(switchboard: Address): Bool { +let switchboardRef = getAccount(switchboard) + .getCapability<&{FungibleTokenSwitchboard.SwitchboardPublic}>(FungibleTokenSwitchboard.PublicPath) + .borrow() + ?? panic("Unable to borrow capability with restricted type of {FungibleTokenSwitchboard.SwitchboardPublic} from ".concat(switchboard.toString()).concat( "account")) + return switchboardRef.checkReceiverByType(type: Type<@ExampleToken.Vault>()) +} \ No newline at end of file diff --git a/transactions/scripts/tokenForwarder/is_recipient_valid.cdc b/transactions/scripts/tokenForwarder/is_recipient_valid.cdc new file mode 100644 index 00000000..45dbf8fe --- /dev/null +++ b/transactions/scripts/tokenForwarder/is_recipient_valid.cdc @@ -0,0 +1,10 @@ +import TokenForwarding from "../../../contracts/utility/TokenForwarding.cdc" + +pub fun main(addr: Address, tokenForwardingPath: PublicPath): Bool { + let forwarderRef = getAccount(addr) + .getCapability<&{TokenForwarding.ForwarderPublic}>(tokenForwardingPath) + .borrow() + ?? panic("Unable to borrow {TokenForwarding.ForwarderPublic} restrict type from a capability") + + return forwarderRef.check() +} \ No newline at end of file diff --git a/transactions/switchboard/safe_transfer_tokens_v2.cdc b/transactions/switchboard/safe_transfer_tokens_v2.cdc new file mode 100644 index 00000000..af7e8914 --- /dev/null +++ b/transactions/switchboard/safe_transfer_tokens_v2.cdc @@ -0,0 +1,45 @@ +import FungibleToken from "./../../contracts/FungibleToken.cdc" +import FungibleTokenSwitchboard from "./../../contracts/FungibleTokenSwitchboard.cdc" +import ExampleToken from "./../../contracts/ExampleToken.cdc" + +// This transaction is a template for a transaction that could be used by anyone +// to send tokens to another account through a switchboard using the deposit +// method but before depositing we will explicitly check whether receiving capability is +// borrowable or not and if yes then it will deposit the vault to the receiver capability. +transaction(to: Address, amount: UFix64) { + + // The reference to the vault from the payer's account + let vaultRef: &ExampleToken.Vault + // The Vault resource that holds the tokens that are being transferred + let sentVault: @FungibleToken.Vault + + prepare(signer: AuthAccount) { + + // Get a reference to the signer's stored vault + self.vaultRef = signer.borrow<&ExampleToken.Vault>(from: ExampleToken.VaultStoragePath) + ?? panic("Could not borrow reference to the owner's Vault!") + + // Withdraw tokens from the signer's stored vault + self.sentVault <-self.vaultRef.withdraw(amount: amount) + + } + + execute { + + // Get the recipient's public account object + let recipient = getAccount(to) + + // Get a reference to the recipient's Switchboard Receiver + let switchboardRef = recipient.getCapability(FungibleTokenSwitchboard.PublicPath) + .borrow<&FungibleTokenSwitchboard.Switchboard{FungibleTokenSwitchboard.SwitchboardPublic}>() + ?? panic("Could not borrow receiver reference to switchboard!") + + // Validate the receiving capability by using safeBorrowByType + if let receivingRef = switchboardRef.safeBorrowByType(type: Type<@ExampleToken.Vault>()){ + switchboardRef.deposit(from: <-self.sentVault) + } else { + destroy self.sentVault + } + } + +} \ No newline at end of file