Skip to content
This repository has been archived by the owner on Jun 7, 2023. It is now read-only.

User deposit address sweeping to internal exchange addresses

Dominik Schiener edited this page Sep 24, 2017 · 2 revisions

What are sweeps?

Sweeps are transfers from user deposit addresses to cold storage or hot wallet. They allow you to safely credit users, after moving funds from their deposit addresses. Sweeps can be sent periodically after scanning addresses for balance.

Why are sweeps important? / When should I credit users?

Sweeps are crucial to securely credit users as it is only secure to hold funds in unspent addresses.

In case a user deposits to an already swept address, an adversary might try to forge a signature and move the funds out of this address if the exchange has already swept from it before. Therefore, funds should be swept to your cold storage or hot wallet periodically.

Crediting should only be performed after sweep transactions confirm, and the funds are in your possession.

You should absolutely inform the user of the risk of total loss should he send to a deposit address provided by the exchange more than once.

How to perform sweeps ?

In this example we define the addresses and related metadata used to create sweep transfers and credit the users.

Determine the correct sweep inputs

Scan deposit addresses for balance

First step is to determine a set of deposit addresses, fetch their balances with iota.api.getBalances() and filter out any empty addresses.

const addresses = [
    'ZRMBFCFLJSEIHYSXGDMOJOCORBWSSGJDJMVZGWUYGXTJYUJQELKOXNWOYBYTMYEIHP9PPPKWDEVOYJPZH',
    'TXPETAIGWJGETC9Y9FKKLHIBZHO9KQESOHZBKEQBEAKJJKDQCSD9CRPKSZIXIGTPMLBVYXLHNNDMZFTSH'
]

// Address metadata
// hashes represent any unresolved sweeps
const metadata = [
    {security: 2, keyIndex: 1, user: 'Chris', hashes: []},
    {security: 3, keyIndex: 2, user: 'Dom', hashes: ['']}
]

iota.api.getBalances(addresses, 100, (err, res) => {
    if (err) {
        // handle error
        console.log(err)
        return
    }
    
    // Parse balances to int
    balances = res.balances.map(balance => parseInt(balance))
    
    // Construct sweep objects
    const sweeps = metadata.map((data, i) => {
        address: addresses[i],
        security: metadata[i].security,
        keyIndex: metadata[i].keyIndex,
        hashes: metadata[i].hashes,
        balance: balances[i]
    })
    
    // Do not sweep deposit addresses with 0 balance
    sweeps.filter(sweep => sweep.balance > 0)
    
    // Check for on-going sweeps related to each deposit address
    // [...]
    
})

Check if deposit addresses were used in unresolved sweeps

To dermine if previous sweeps of any deposit address are on-going we use isReattachable() on sweep tail transaction hashes.

// Extract first tail hash out of each addresses
const hashes = [] 
sweeps.forEach(sweep => hashes.push(sweep.hashes[0]))

// Check for on-going sweeps related to each address
iota.api.isReattachable({hashes: hashes}, (err, res) => {
    if (err) {
        // handle error
        console.log(err)
        return
    }
    
    // Do not sweep addresses that were used as inputs in pending sweeps
    sweeps = sweeps.filter(sweep => {
        const index = hashes.indexOf(hash => hash === sweep.hashes[0])
        if (index === -1) {
            return true
        }
        return !res[index];
    })
    
    // Construct inputs
    const inputs = sweeps.map(sweep => {
        address: sweep.address,
        security: sweep.security,
        keyIndex: sweep.keyIndex,
        balance: sweep.balance
    })
    
    // Send the sweep transfer

    // [...]
})
 

Warning: If any addresses have been previously swept, do not sweep them again, unless previous sweep transfer has been confirmed.

Sending transfers

Then use sendTransfer() and provide the selected addresses as inputs to move their balance to an unspent exchange owned address. In this example we aggregate inputs assigned to different users in a single transaction, improving performance by reducing amount of Proof-of-Work.

// Define an unused, exchange owned address as output of the sweep
// Make sure that no other transfer will use this address as input
const destinationAddress = 'KTLACMXXOJYIIEYJJYSUXXOJUPSVNLQHZSSLWSBOSPALSDAZQNPZBPNMCFZDVS9RGJRKQHBDEZZGGWUQE'

// Define options by passing inputs and remainder address
const options = {
    inputs: inputs,
    // Remainder address must always be an unused, exchange owned address when sending sweep transfers.
    // In this example we simply use above `destinationAddress`
    address: destinationAddress
}

// Sum up all swept balances
const totalValue = sweeps.reduce((sum, sweep) => sum + sweep.balance, 0)

// Define transfers using the destination address and the sum of input balances as the value
const transfers = [{
     'address': destinationAddress,
     'value': totalValue,
     'message': '',
     'tag': ''
}]

// Send transfer and obtain the tail hash
iota.api.sendTransfer(seed, 4, 15, transfers, options, (err, transactions) => {
     if (err) {
         // handle error
         console.log(err)
         return
     }
     
     // Extract the tail transaction hash from the response
     const tailHash = transactions[0].hash
     console.log(tailHash)
     
     // Save tail hash to be used for properly whitelisting addresses to be swept and for checking confirmation of the sweep
     inputs.forEach(input => {
        const index = metadata.indexOf(data => data.address === input.address)
        // Update latest sweep transaction tail hash
        metadata[index].hashes[0] = tailHash
     })
     
     metadata.push(tailHash)
})

Notice: While aggregating sweeps improves performance, it is advised to keep bundle size limited to 10 transactions on average for having faster confirmations. This is achieved by aggregating 4 inputs in a bundle on average.

Warning: All input addresses passed to a signle sendTransfer() call must be generated from the same seed. You might want to cache addresses belonging to a seed together with their key index because address generation is computationally expensive.

Warning: Make sure to provide the correct security level and key index for each input.

Warning: Wait for sweep transfers to confirm before spending their outputs.

Warning: Always define an unspent exchange owned remainder address. Ideally, the remainder address should be same as the output address.

How can/should I credit users?

Checking for confirmed sweeps

Last step to ensure secure deposit of funds to exchange addresses is to check the inclusion states of the sweep transactions. Make use of the getLatestInclusion() and pass the tail transaction hashes. The result determines which transactions are confirmed and accepted by the network.

// Extract tail hashes, obtained by `sendTrasnfer()`, from metadata
const hashes = metadata.map(data => data.hashes)
    .reduce((hashes, hash) => hashes.concat(hash), [])
    
// Check inclusion states
iota.api.getLatestInclusion(hashes, (err, states) => {
    if (err) {
        // handle error
        console.log(err)
        return
    }
    states.forEach((confirmed, i) => {
        if (confirmed) {
            console.log('Transaction with tail hash:', hashes[i], 'confirmed')
            
            // credit he user
            // [...]
        }
    })
})

Notice: You might need to reattach sweeps that remain unconfirmed after a certain time frame. Check Transaction reattachments guide for detailed instructions.

Warning: Proceed to crediting only for transactions that have been confirmed.

Crediting the users

Sweeps from addresses that belong to different users could be aggregated and sent with a single call to sendTransfer() for improved performance, as shown before, but should be credited carefully. To credit the user fetch the bundle of the sweep transfer with getBundle() by passing the tail transaction hash. Then examine each transaction in the bundle for negative value and credit based on value and address.

iota.api.getLatestInclusion(hashes, (err, states) => {
    if (err) {
       // handle error
       console.log(err)
       return
    }
    states.forEach((confirmed, i) => {
	if (confirmed) {
	    // Get bundle by tail hash
	    iota.api.getBundle(hashes[i], (err, bundle) => {
		if (err) {
		    // handle error
                    console.log(err)
		    return
		}
		// Check address and value for each transaction with negative value to credit the user
		bundle
		    .filter(tx => tx.value < 0)
		    .forEach(tx => {
		        // get index of transaction address in addresses array
		        const j = addresses.findIndex(address => address === tx.address)
		        if (j !== -1) {
		            // Find user that deposited to the specific swept address
		            const user = metadata[j].user
		            // Get the absolute value of the sweep
	                    const value = Math.abs(tx.value)
		            // credit `user` with `value`
			    console.log("credit " + user + " with " + value)
		        }
		    })
	    })
	}
    })
})

Notice: We only need to check the tail transaction hash with getLatestInclusion(). Bundles are atomic sets of transactions, as such transactions in the bundle are either all confirmed or unconfirmed.

Warning: Other approaches on crediting of aggregated sweeps may result to wrong credits; for example basing credit on getBalances() prior to sweep, has a time-of-check time-of-use issue, If a user has (successfully) deposited while building the sweep.