import { CeloTxPending , CeloTxReceipt , TransactionResult } from '@celo/connect' ; import { ContractKit , newKitFromWeb3 } from '@celo/contractkit' ; import { CeloTokenType , EachCeloToken , StableToken , Token } from '@celo/contractkit/lib/celo-tokens' ; import { eqAddress } from '@celo/utils/lib/address' ; import { toFixed } from '@celo/utils/lib/fixidity' ; import BigNumber from 'bignumber.js' ; import { assert } from 'chai' ; import Web3 from 'web3' ; import { GethInstanceConfig } from '../lib/interfaces/geth-instance-config' ; import { GethRunConfig } from '../lib/interfaces/geth-run-config' ; import { getHooks , initAndSyncGethWithRetry , killInstance , sleep } from './utils' ; const TMP_PATH = '/tmp/e2e' ; const verbose = false ; /**
 * Helper Class to change StableToken Inflation in tests
 */ class InflationManager { private kit : ContractKit ; private readonly minUpdateDelay = 10 ; constructor ( readonly validatorUri , readonly validatorAddress , readonly token ) { this . kit = newKitFromWeb3 ( new Web3 ( validatorUri ) ) ; this . kit . connection . defaultAccount = validatorAddress ; } now = async ( ) : Promise < number > => { return Number ( ( await this . kit . connection . getBlock ( 'pending' ) ) . timestamp ) ; } ; getNextUpdateRate = async ( ) : Promise < number > => { const stableToken = await this . getStableToken ( ) ; const { factorLastUpdated } = await stableToken . getInflationParameters ( ) ; let timeSinceLastUpdated = ( await this . now ( ) ) - factorLastUpdated . toNumber ( ) ; while ( timeSinceLastUpdated < this . minUpdateDelay ) { await sleep ( this . minUpdateDelay - timeSinceLastUpdated ) ; timeSinceLastUpdated = ( await this . now ( ) ) - factorLastUpdated . toNumber ( ) ; } return timeSinceLastUpdated ; } ; getParameters = async ( ) => { const stableToken = await this . getStableToken ( ) ; return stableToken . getInflationParameters ( ) ; } ; setInflationRateForNextTransfer = async ( rate ) => { await this . setInflationParameters ( new BigNumber ( 1 ) , Number . MAX_SAFE_INTEGER ) ; const updateRate = await this . getNextUpdateRate ( ) ; await this . setInflationParameters ( rate , updateRate ) ; } ; setInflationParameters = async ( rate , updatePeriod ) => { const stableToken = await this . getStableToken ( ) ; await stableToken . setInflationParameters ( toFixed ( rate ) . toFixed ( ) , updatePeriod . toFixed ( ) ) . sendAndWaitForReceipt ( { from : this . validatorAddress } ) ; } ; getStableToken = async ( ) => { return this . kit . celoTokens . getWrapper ( this . token ) ; } ; } const setIntrinsicGas = async ( validatorUri , validatorAddress , gasCost ) => { const kit = newKitFromWeb3 ( new Web3 ( validatorUri ) ) ; const parameters = await kit . contracts . getBlockchainParameters ( ) ; await parameters . setIntrinsicGasForAlternativeFeeCurrency ( gasCost . toString ( ) ) . sendAndWaitForReceipt ( { from : validatorAddress } ) ; } ; const INTRINSIC_TX_GAS_COST = 21000 ; const ADDITIONAL_INTRINSIC_TX_GAS_COST = 50000 ; const sstoreCleanRefundEIP2200 = 4200 ; const stableTokenTransferGasCost = 23653 ; /** Helper to watch balance changes over accounts */ interface BalanceWatcher { update ( ) : Promise < void > ; delta ( address , token ) : BigNumber ; current ( address , token ) : BigNumber ; initial ( address , token ) : BigNumber ; debugPrint ( address , token ) : void ; } async function newBalanceWatcher ( kit , accounts ) { async function fetch ( ) { const balances = { } ; await Promise . all ( accounts . map ( async ( a ) => { balances [ a ] = await kit . celoTokens . balancesOf ( a ) ; } ) ) ; return balances ; } const initial = await fetch ( ) ; let current = initial ; return { async update ( ) { current = await fetch ( ) ; } , delta ( address , token ) { return ( current [ address ] [ token ] || new BigNumber ( 0 ) ) . minus ( initial [ address ] [ token ] || 0 ) ; } , current ( address , token ) { return current [ address ] [ token ] || new BigNumber ( 0 ) ; } , initial ( address , token ) { return initial [ address ] [ token ] || new BigNumber ( 0 ) ; } , debugPrint ( address , token ) { console . log ( { initial : initial [ address ] [ token ] ?. toString ( ) , current : current [ address ] [ token ] ?. toString ( ) , delta : ( current [ address ] [ token ] || new BigNumber ( 0 ) ) . minus ( initial [ address ] [ token ] || 0 ) . toString ( ) , } ) ; } , } ; } function assertEqualBN ( value , expected ) { assert . equal ( value . toString ( ) , expected . toString ( ) ) ; } describe ( 'Transfer tests' , function ( this ) { this . timeout ( 0 ) ; let kit ; const TransferAmount = new BigNumber ( Web3 . utils . toWei ( '1' , 'ether' ) ) ; let currentGethInstance ; let governanceAddress ; const validatorAddress = '0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95' ; const DEF_FROM_PK = 'f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d' ; const FromAddress = '0x5409ed021d9299bf6814279a6a1411a7e866a631' ; const txFeeRecipientAddress = '0x5555555555555555555555555555555555555555' ; const ToAddress = '0xbBae99F0E1EE565404465638d40827b54D343638' ; const gatewayFeeRecipientAddress = '0x4f5f8a3f45d179553e7b95119ce296010f50f6f1' ; const syncModes = [ 'full' , 'fast' , 'light' , 'lightest' ] ; const gethConfig = { migrate : true , networkId : 1101 , network : 'local' , runPath : TMP_PATH , genesisConfig : { churritoBlock : 0 , donutBlock : 0 , } , instances : [ { name : 'validator' , validating : true , minerValidator : validatorAddress , txFeeRecipient : txFeeRecipientAddress , syncmode : 'full' , port : 30303 , rpcport : 8545 , } , ] , } ; const hooks = getHooks ( gethConfig ) ; before ( async function ( this ) { this . timeout ( 0 ) ; await hooks . before ( ) ; } ) ; after ( async function ( this ) { this . timeout ( 0 ) ; await hooks . after ( ) ; } ) ; const fullInstance = { name : 'txFull' , validating : false , syncmode : 'full' , lightserv : true , gatewayFee : new BigNumber ( 10000 ) , port : 30305 , rpcport : 8547 , minerValidator : gatewayFeeRecipientAddress , txFeeRecipient : gatewayFeeRecipientAddress , } ; const restartWithCleanNodes = async ( ) => { await hooks . restart ( ) ; kit = newKitFromWeb3 ( new Web3 ( 'http://localhost:8545' ) ) ; kit . connection . defaultGasInflationFactor = 1 ; await sleep ( 2 ) ; await kit . connection . web3 . eth . personal . unlockAccount ( validatorAddress , '' , 1000000 ) ; await initAndSyncGethWithRetry ( gethConfig , hooks . gethBinaryPath , fullInstance , [ ... gethConfig . instances , fullInstance ] , verbose , 3 ) ; governanceAddress = ( await kit . _web3Contracts . getGovernance ( ) ) . options . address ; const epochRewards = await kit . _web3Contracts . getEpochRewards ( ) ; await epochRewards . methods . setCommunityRewardFraction ( 0 ) . send ( { from : validatorAddress } ) ; const startBalance = TransferAmount . times ( 500 ) ; const resDollars = await transferCeloDollars ( validatorAddress , FromAddress , startBalance ) ; const resGold = await transferCeloGold ( validatorAddress , FromAddress , startBalance ) ; await Promise . all ( [ resDollars . waitReceipt ( ) , resGold . waitReceipt ( ) ] ) ; } ; const startSyncNode = async ( syncmode ) => { if ( currentGethInstance != null ) { await killInstance ( currentGethInstance ) ; } const light = syncmode === 'light' || syncmode === 'lightest' ; currentGethInstance = { name : syncmode , validating : false , syncmode , port : 30307 , rpcport : 8549 , lightserv : ! light , gatewayFee : light ? new BigNumber ( 10000 ) : undefined , privateKey : DEF_FROM_PK , } ; await initAndSyncGethWithRetry ( gethConfig , hooks . gethBinaryPath , currentGethInstance , [ fullInstance , currentGethInstance ] , verbose , 3 ) ; kit . connection . setProvider ( new Web3 . providers . HttpProvider ( 'http://localhost:8549' ) ) ; const upstream = await new Web3 ( 'http://localhost:8545' ) . eth . getBlock ( 'latest' ) ; while ( ( await kit . connection . getBlock ( 'latest' ) ) . number < upstream . number ) { await sleep ( 0.5 ) ; } await kit . connection . web3 . eth . personal . unlockAccount ( FromAddress , '' , 1000000 ) ; } ; const transferCeloGold = async ( fromAddress , toAddress , amount , txOptions = { } ) => { const res = await kit . connection . sendTransaction ( { from : fromAddress , to : toAddress , value : amount . toString ( ) , ... txOptions , } ) ; return res ; } ; const transferCeloDollars = async ( fromAddress , toAddress , amount , txOptions = { } ) => { const kitStableToken = await kit . contracts . getStableToken ( ) ; const res = await kitStableToken . transfer ( toAddress , amount . toString ( ) ) . send ( { from : fromAddress , ... txOptions , } ) ; return res ; } ; const getGasPriceMinimum = async ( feeCurrency ) => { const gasPriceMinimum = await kit . _web3Contracts . getGasPriceMinimum ( ) ; if ( feeCurrency ) { return gasPriceMinimum . methods . getGasPriceMinimum ( feeCurrency ) . call ( ) ; } else { return gasPriceMinimum . methods . gasPriceMinimum ( ) . call ( ) ; } } ; interface Fees { total : BigNumber ; tip : BigNumber ; base : BigNumber ; gateway : BigNumber ; } interface GasUsage { used ? : number ; expected : number ; } interface TestTxResults { ok : boolean ; fees : Fees ; gas : GasUsage ; events : any [ ] ; } const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' ; function truncateTopic ( hex ) { return '0x' + hex . substr ( 26 ) ; } function parseEvents ( receipt ) { if ( ! receipt ) { return [ ] ; } if ( receipt . events && receipt . events . Transfer ) { let events = receipt . events . Transfer ; if ( ! ( events instanceof Array ) ) { events = [ events ] ; } return events . map ( ( a ) => ( { to : a . returnValues . to , from : a . returnValues . from } ) ) ; } if ( receipt . logs ) { return receipt . logs . filter ( ( a ) => a . topics [ 0 ] === TRANSFER_TOPIC ) . map ( ( a ) => ( { to : truncateTopic ( a . topics [ 2 ] ) , from : truncateTopic ( a . topics [ 1 ] ) } ) ) ; } } const runTestTransaction = async ( txResult , expectedGasUsed , feeCurrency ? ) : Promise < TestTxResults > => { const minGasPrice = await getGasPriceMinimum ( feeCurrency ) ; assert . isAbove ( parseInt ( minGasPrice , 10 ) , 0 ) ; let ok = false ; let receipt ; try { receipt = await txResult . waitReceipt ( ) ; ok = true ; } catch ( err ) { ok = false ; } const events = parseEvents ( receipt ) ; if ( receipt != null && receipt . gasUsed !== expectedGasUsed ) { console . log ( 'OOPSS: Different Gas' , receipt . gasUsed , expectedGasUsed ) ; } const gasVal = receipt ? receipt . gasUsed : expectedGasUsed ; assert . isAbove ( gasVal , 0 ) ; const txHash = await txResult . getHash ( ) ; const tx = await kit . connection . getTransaction ( txHash ) ; assert . isAbove ( parseInt ( tx . gasPrice , 10 ) , 0 ) ; const txFee = new BigNumber ( gasVal ) . times ( tx . gasPrice ) ; const txFeeBase = new BigNumber ( gasVal ) . times ( minGasPrice ) ; const txFeeTip = txFee . minus ( txFeeBase ) ; const gatewayFee = new BigNumber ( tx . gatewayFee || 0 ) ; assert . equal ( tx . gatewayFeeRecipient === null , gatewayFee . eq ( 0 ) ) ; const fees = { total : txFee . plus ( gatewayFee ) , base : txFeeBase , tip : txFeeTip , gateway : gatewayFee , } ; const gas = { used : receipt && receipt . gasUsed , expected : expectedGasUsed , } ; return { ok , fees , gas , events } ; } ; function testTxPoolFiltering ( { feeToken , gas , expectedError , } ) { it ( 'should not add the transaction to the pool' , async ( ) => { const feeCurrency = await kit . celoTokens . getFeeCurrencyAddress ( feeToken ) ; try { const res = await transferCeloGold ( FromAddress , ToAddress , TransferAmount , { gas , feeCurrency , } ) ; await res . waitReceipt ( ) ; assert . fail ( 'no error was thrown' ) ; } catch ( error ) { assert . include ( error . toString ( ) , expectedError ) ; } } ) ; } function testTransferToken ( { transferToken , feeToken , expectedGas , txOptions , expectSuccess = true , fromAddress = FromAddress , toAddress = ToAddress , } ) { let txRes ; let balances ; before ( async ( ) => { const feeCurrency = await kit . celoTokens . getFeeCurrencyAddress ( feeToken ) ; const accounts = [ fromAddress , toAddress , txFeeRecipientAddress , gatewayFeeRecipientAddress , governanceAddress , ] ; balances = await newBalanceWatcher ( kit , accounts ) ; const transferFn = transferToken === StableToken . cUSD ? transferCeloDollars : transferCeloGold ; const txResult = await transferFn ( fromAddress , toAddress , TransferAmount , { ... txOptions , feeCurrency , } ) ; if ( kit . celoTokens . isStableToken ( transferToken ) && balances . initial ( toAddress , transferToken ) . eq ( 0 ) ) { expectedGas += 15000 ; } txRes = await runTestTransaction ( txResult , expectedGas , feeCurrency ) ; await balances . update ( ) ; } ) ; if ( expectSuccess ) { it ( `should succeed` , ( ) => assert . isTrue ( txRes . ok ) ) ; it ( `should use the expected amount of gas` , ( ) => assert . equal ( txRes . gas . used , txRes . gas . expected ) ) ; it ( `should increment the receiver's ${ transferToken } balance by the transfer amount` , ( ) => assertEqualBN ( balances . delta ( toAddress , transferToken ) , TransferAmount ) ) ; it ( 'should have emitted transfer events for the fee token if not using CELO' , ( ) => { if ( kit . celoTokens . isStableToken ( feeToken ) ) { assert ( txRes . events . find ( ( a ) => eqAddress ( a . to , governanceAddress ) && eqAddress ( a . from , fromAddress ) ) ) ; } } ) ; if ( transferToken === feeToken ) { it ( `should decrement the sender's ${ transferToken } balance by the transfer amount plus fees` , ( ) => { const expectedBalanceChange = txRes . fees . total . plus ( TransferAmount ) ; assertEqualBN ( balances . delta ( fromAddress , transferToken ) . negated ( ) , expectedBalanceChange ) ; } ) ; } else { it ( `should decrement the sender's ${ transferToken } balance by the transfer amount` , ( ) => assertEqualBN ( balances . delta ( fromAddress , transferToken ) . negated ( ) , TransferAmount ) ) ; it ( `should decrement the sender's ${ feeToken } balance by the total fees` , ( ) => assertEqualBN ( balances . delta ( fromAddress , feeToken ) . negated ( ) , txRes . fees . total ) ) ; } } else { it ( `should fail` , ( ) => assert . isFalse ( txRes . ok ) ) ; it ( `should decrement the sender's ${ feeToken } balance by the total fees` , ( ) => assertEqualBN ( balances . delta ( fromAddress , feeToken ) . negated ( ) , txRes . fees . total ) ) ; it ( `should not change the receiver's ${ transferToken } balance` , ( ) => { assertEqualBN ( balances . initial ( toAddress , transferToken ) , balances . current ( toAddress , transferToken ) ) ; } ) ; if ( transferToken !== feeToken ) { it ( `should not change the sender's ${ transferToken } balance` , ( ) => { assertEqualBN ( balances . initial ( fromAddress , transferToken ) , balances . current ( fromAddress , transferToken ) ) ; } ) ; } } it ( `should increment the gateway fee recipient's ${ feeToken } balance by the gateway fee` , ( ) => assertEqualBN ( balances . delta ( gatewayFeeRecipientAddress , feeToken ) , txRes . fees . gateway ) ) ; it ( `should increment the infrastructure fund's ${ feeToken } balance by the base portion of the gas fee` , ( ) => assertEqualBN ( balances . delta ( governanceAddress , feeToken ) , txRes . fees . base ) ) ; it ( `should increment the tx fee recipient's ${ feeToken } balance by the rest of the gas fee` , ( ) => { assertEqualBN ( balances . delta ( txFeeRecipientAddress , feeToken ) , txRes . fees . tip ) ; } ) ; } describe ( 'Normal Transfer >' , ( ) => { before ( restartWithCleanNodes ) ; for ( const syncMode of syncModes ) { describe ( `${ syncMode } Node >` , ( ) => { before ( `start geth on sync: ${ syncMode }` , ( ) => startSyncNode ( syncMode ) ) ; describe ( 'Transfer CeloGold >' , ( ) => { describe ( 'with feeCurrency = CeloGold >' , ( ) => { if ( syncMode === 'light' || syncMode === 'lightest' ) { describe ( 'when running in light/lightest sync mode' , ( ) => { const recipient = ( choice ) => { switch ( choice ) { case 'peer' : return gatewayFeeRecipientAddress ; case 'random' : return Web3 . utils . randomHex ( 20 ) ; default : return undefined ; } } ; const feeValue = ( choice ) => { switch ( choice ) { case 'sufficient' : return '0x10000' ; case 'insufficient' : return '0x1' ; default : return undefined ; } } ; for ( const recipientChoice of [ 'peer' , 'random' , 'unset' ] ) { describe ( `when the gateway fee recipient is ${ recipientChoice }` , ( ) => { for ( const feeValueChoice of [ 'sufficient' , 'insufficient' , 'unset' ] ) { describe ( `when the gateway fee value is ${ feeValueChoice }` , ( ) => { const txOptions = { gatewayFeeRecipient : recipient ( recipientChoice ) , gatewayFee : feeValue ( feeValueChoice ) , } ; if ( recipientChoice === 'random' || feeValueChoice === 'insufficient' ) { it ( 'should get rejected by the sending node before being added to the tx pool' , async ( ) => { try { const res = await transferCeloGold ( FromAddress , ToAddress , TransferAmount , txOptions ) ; await res . waitReceipt ( ) ; assert . fail ( 'no error was thrown' ) ; } catch ( error ) { assert . include ( error . toString ( ) , `Error: no suitable peers available` ) ; } } ) ; } else { testTransferToken ( { expectedGas : INTRINSIC_TX_GAS_COST , transferToken : Token . CELO , feeToken : Token . CELO , txOptions , } ) ; } } ) ; } } ) ; } } ) ; } else { testTransferToken ( { expectedGas : INTRINSIC_TX_GAS_COST , transferToken : Token . CELO , feeToken : Token . CELO , } ) ; } } ) ; describe ( 'feeCurrency = CeloDollars >' , ( ) => { const intrinsicGas = INTRINSIC_TX_GAS_COST + ADDITIONAL_INTRINSIC_TX_GAS_COST ; describe ( 'when there is no demurrage' , ( ) => { describe ( 'when setting a gas amount greater than the amount of gas necessary' , ( ) => testTransferToken ( { expectedGas : intrinsicGas , transferToken : Token . CELO , feeToken : StableToken . cUSD , } ) ) ; describe ( 'when setting a gas amount less than the intrinsic gas amount' , ( ) => { it ( 'should not add the transaction to the pool' , async ( ) => { const gas = intrinsicGas - 1 ; const feeCurrency = await kit . celoTokens . getFeeCurrencyAddress ( StableToken . cUSD ) ; try { const res = await transferCeloGold ( FromAddress , ToAddress , TransferAmount , { gas , feeCurrency , } ) ; await res . getHash ( ) ; assert . fail ( 'no error was thrown' ) ; } catch ( error ) { assert . include ( error . toString ( ) , 'Error: intrinsic gas too low' ) ; } } ) ; } ) ; } ) ; } ) ; } ) ; describe ( 'Transfer CeloDollars' , ( ) => { describe ( 'feeCurrency = CeloDollars >' , ( ) => { testTransferToken ( { expectedGas : stableTokenTransferGasCost + INTRINSIC_TX_GAS_COST + ADDITIONAL_INTRINSIC_TX_GAS_COST , transferToken : StableToken . cUSD , feeToken : StableToken . cUSD , } ) ; } ) ; describe ( 'feeCurrency = CeloGold >' , ( ) => { testTransferToken ( { expectedGas : stableTokenTransferGasCost + INTRINSIC_TX_GAS_COST + sstoreCleanRefundEIP2200 , transferToken : StableToken . cUSD , feeToken : Token . CELO , } ) ; } ) ; } ) ; } ) ; } } ) ; describe ( 'Transfer with changed intrinsic gas cost >' , ( ) => { const changedIntrinsicGasForAlternativeFeeCurrency = 34000 ; before ( restartWithCleanNodes ) ; for ( const syncMode of syncModes ) { describe ( `${ syncMode } Node >` , ( ) => { before ( `start geth on sync: ${ syncMode }` , async ( ) => { try { await startSyncNode ( syncMode ) ; await setIntrinsicGas ( 'http://localhost:8545' , validatorAddress , changedIntrinsicGasForAlternativeFeeCurrency ) ; } catch ( err ) { console . debug ( 'some error' , err ) ; } } ) ; describe ( 'Transfer CeloGold >' , ( ) => { describe ( 'feeCurrency = CeloDollars >' , ( ) => { const intrinsicGas = changedIntrinsicGasForAlternativeFeeCurrency + INTRINSIC_TX_GAS_COST ; describe ( 'when there is no demurrage' , ( ) => { describe ( 'when setting a gas amount greater than the amount of gas necessary' , ( ) => testTransferToken ( { expectedGas : intrinsicGas , transferToken : Token . CELO , feeToken : StableToken . cUSD , } ) ) ; describe ( 'when setting a gas amount less than the intrinsic gas amount' , ( ) => { testTxPoolFiltering ( { gas : intrinsicGas - 1 , feeToken : StableToken . cUSD , expectedError : 'Error: intrinsic gas too low' , } ) ; } ) ; } ) ; } ) ; } ) ; describe ( 'Transfer CeloDollars' , ( ) => { describe ( 'feeCurrency = CeloDollars >' , ( ) => { testTransferToken ( { expectedGas : stableTokenTransferGasCost + changedIntrinsicGasForAlternativeFeeCurrency + INTRINSIC_TX_GAS_COST , transferToken : StableToken . cUSD , feeToken : StableToken . cUSD , } ) ; } ) ; } ) ; } ) ; } } ) ; describe ( 'Transfer with Demurrage >' , ( ) => { for ( const syncMode of syncModes ) { describe ( `${ syncMode } Node >` , ( ) => { let inflationManager ; before ( `start geth on sync: ${ syncMode }` , async ( ) => { await restartWithCleanNodes ( ) ; inflationManager = new InflationManager ( 'http://localhost:8545' , validatorAddress , StableToken . cUSD ) ; await startSyncNode ( syncMode ) ; } ) ; describe ( 'when there is demurrage of 50% applied' , ( ) => { describe ( 'when setting a gas amount greater than the amount of gas necessary' , ( ) => { let balances ; let expectedFees ; let txRes ; before ( async ( ) => { balances = await newBalanceWatcher ( kit , [ FromAddress , ToAddress , gatewayFeeRecipientAddress , governanceAddress , ] ) ; await inflationManager . setInflationRateForNextTransfer ( new BigNumber ( 2 ) ) ; const feeCurrency = await kit . celoTokens . getFeeCurrencyAddress ( StableToken . cUSD ) ; txRes = await runTestTransaction ( await transferCeloGold ( FromAddress , ToAddress , TransferAmount , { feeCurrency , } ) , INTRINSIC_TX_GAS_COST + ADDITIONAL_INTRINSIC_TX_GAS_COST , feeCurrency ) ; await balances . update ( ) ; expectedFees = txRes . fees ; } ) ; it ( 'should succeed' , ( ) => assert . isTrue ( txRes . ok ) ) ; it ( 'should use the expected amount of gas' , ( ) => assert . equal ( txRes . gas . used , txRes . gas . expected ) ) ; it ( "should decrement the sender's Celo Gold balance by the transfer amount" , ( ) => { assertEqualBN ( balances . delta ( FromAddress , Token . CELO ) . negated ( ) , TransferAmount ) ; } ) ; it ( "should increment the receiver's Celo Gold balance by the transfer amount" , ( ) => { assertEqualBN ( balances . delta ( ToAddress , Token . CELO ) , TransferAmount ) ; } ) ; it ( "should halve the sender's Celo Dollar balance due to demurrage and decrement it by the total fees" , ( ) => { assertEqualBN ( balances . initial ( FromAddress , StableToken . cUSD ) . idiv ( 2 ) . minus ( balances . current ( FromAddress , StableToken . cUSD ) ) , expectedFees . total ) ; } ) ; it ( "should halve the gateway fee recipient's Celo Dollar balance then increase it by the gateway fee" , ( ) => { assertEqualBN ( balances . current ( gatewayFeeRecipientAddress , StableToken . cUSD ) . minus ( balances . initial ( gatewayFeeRecipientAddress , StableToken . cUSD ) . idiv ( 2 ) ) , expectedFees . gateway ) ; } ) ; it ( "should halve the infrastructure fund's Celo Dollar balance then increment it by the base portion of the gas fees" , ( ) => { assertEqualBN ( balances . current ( governanceAddress , StableToken . cUSD ) . minus ( balances . initial ( governanceAddress , StableToken . cUSD ) . idiv ( 2 ) ) , expectedFees . base ) ; } ) ; } ) ; } ) ; } ) ; } } ) ; } ) ;