diff --git a/package.json b/package.json index 1015812..ac7d17f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rsksmart/rif-relay-client", - "version": "2.2.5", + "version": "2.2.6", "private": false, "description": "This project contains all the client code for the rif relay system.", "license": "MIT", @@ -90,7 +90,7 @@ "typescript": "4.8.2" }, "peerDependencies": { - "@rsksmart/rif-relay-contracts": "2.1.1-beta.1", + "@rsksmart/rif-relay-contracts": "2.1.1-beta.2", "ethers": "^5.7.0" }, "publishConfig": { diff --git a/src/gasEstimator/gasEstimator.ts b/src/gasEstimator/gasEstimator.ts index 0901aa6..1bcd56a 100644 --- a/src/gasEstimator/gasEstimator.ts +++ b/src/gasEstimator/gasEstimator.ts @@ -5,16 +5,21 @@ import type { EnvelopingTxRequest } from '../common/relayTransaction.types'; import { POST_RELAY_DEPLOY_GAS_COST, PRE_RELAY_GAS_COST, - POST_DEPLOY_EXECUTION_FACTOR, resolveSmartWalletAddress, standardMaxPossibleGasEstimation, + STORAGE_REFUND, + POST_DEPLOY_EXECUTION, + POST_DEPLOY_NO_EXECUTION, } from './utils'; import { getProvider, type EnvelopingRequest, type RelayTxOptions, } from '../common'; -import { RelayHub__factory } from '@rsksmart/rif-relay-contracts'; +import { + BaseSmartWalletFactory__factory, + RelayHub__factory, +} from '@rsksmart/rif-relay-contracts'; import { signEnvelopingRequest } from '../signer'; const estimateRelayMaxPossibleGas = async ( @@ -63,10 +68,16 @@ const estimateRelayMaxPossibleGas = async ( * - preEnvelopedTxEstimation: the gas required without the payment and the internal call * - paymentEstimation: the gas required for the payment * - internalEstimation: the gas required for the internal call - * - POST_DEPLOY_NO_EXECUTION: when there is no execution in deployment, an additional 4_000 are needed. In the default - * we need to subtract 3500 to avoid over estimating the tx - * - POST_RELAY_DEPLOY_GAS_COST: transfer back cost is always included since we cannot know if the smart wallet is going - * to have some balance after the execution + * - POST_DEPLOY_NO_EXECUTION: when there is no execution in deployment, an additional 4_000 are needed + * - POST_DEPLOY_EXECUTION: when there is execution in deployment, an additional 1_500 are needed + * - STORAGE_REFUND: After the nonces were already touched by a smart wallet deployment or smart wallet execution, we need to subtract 15_000 + * - Manually subtract: + * - Without signature, we cannot analyze if some of the gas costs should be included or not, to avoid underestimating the relay transaction + * the costs are included in all of them. These costs can be subtract by using the following constants: + * - POST_RELAY_DEPLOY_GAS_COST: The transfer back cost is always included since we cannot know if the smart wallet is going + * to have some balance after the execution + * - ACCOUNT_ALREADY_CREATED: The owner address of the smart wallet was previously created. A utility function was included to check + * if the owner address of smart wallet was already created * @param relayRequest * @param signer * @param options @@ -96,18 +107,24 @@ const estimateRelayMaxPossibleGasNoSignature = async ( const isDeploy = isDeployRequest(relayRequest); if (isDeploy) { const from = signer.address; + const provider = getProvider(); + const factory = BaseSmartWalletFactory__factory.connect( + await relayRequest.relayData.callForwarder, + provider + ); + const nonce = await factory.nonce(from); const updatedRelayRequest = { request: { ...relayRequest.request, from, to: constants.AddressZero, + nonce, }, relayData: { ...relayRequest.relayData, }, }; const signature = signEnvelopingRequest(updatedRelayRequest, from, signer); - const provider = getProvider(); const relayHub = RelayHub__factory.connect( await request.relayHub, provider @@ -150,12 +167,17 @@ const estimateRelayMaxPossibleGasNoSignature = async ( let missingGas = 0; if (isDeploy) { if (to == constants.AddressZero) { - missingGas = POST_DEPLOY_EXECUTION_FACTOR * 8; + missingGas = POST_DEPLOY_NO_EXECUTION; } else { - missingGas = POST_DEPLOY_EXECUTION_FACTOR * 3; + missingGas = POST_DEPLOY_EXECUTION; } } + const nonce = BigNumber.from(await request.nonce); + if (!nonce.isZero()) { + missingGas -= STORAGE_REFUND; + } + return preEnvelopedTxEstimation .add(paymentEstimation) .add(internalEstimation) diff --git a/src/gasEstimator/index.ts b/src/gasEstimator/index.ts index 3a17f28..df386d1 100644 --- a/src/gasEstimator/index.ts +++ b/src/gasEstimator/index.ts @@ -4,6 +4,8 @@ export { } from './gasEstimator'; export { standardMaxPossibleGasEstimation, + isAccountCreated, PRE_RELAY_GAS_COST, POST_RELAY_DEPLOY_GAS_COST, + ACCOUNT_ALREADY_CREATED, } from './utils'; diff --git a/src/gasEstimator/utils.ts b/src/gasEstimator/utils.ts index 0cffc5d..5a96454 100644 --- a/src/gasEstimator/utils.ts +++ b/src/gasEstimator/utils.ts @@ -12,7 +12,10 @@ import type { RelayTxOptions } from '../common'; const PRE_RELAY_GAS_COST = 74_000; const POST_RELAY_DEPLOY_GAS_COST = 33_500; -const POST_DEPLOY_EXECUTION_FACTOR = 500; +const POST_DEPLOY_EXECUTION = 1_500; +const POST_DEPLOY_NO_EXECUTION = 4_000; +const STORAGE_REFUND = 15_000; +const ACCOUNT_ALREADY_CREATED = 25_000; const standardMaxPossibleGasEstimation = async ( { relayRequest, metadata: { signature } }: EnvelopingTxRequest, @@ -76,10 +79,22 @@ const resolveSmartWalletAddress = async ( : callForwarder; }; +const isAccountCreated = async (address: string) => { + const provider = getProvider(); + const ownerBalance = await provider.getBalance(address); + const ownerTx = await provider.getTransactionCount(address); + + return !ownerBalance.isZero() || ownerTx > 0; +}; + export { standardMaxPossibleGasEstimation, resolveSmartWalletAddress, + isAccountCreated, PRE_RELAY_GAS_COST, POST_RELAY_DEPLOY_GAS_COST, - POST_DEPLOY_EXECUTION_FACTOR, + POST_DEPLOY_EXECUTION, + POST_DEPLOY_NO_EXECUTION, + STORAGE_REFUND, + ACCOUNT_ALREADY_CREATED, }; diff --git a/test/gasEstimator/gasEstimator.test.ts b/test/gasEstimator/gasEstimator.test.ts index 95bd685..1378bf9 100644 --- a/test/gasEstimator/gasEstimator.test.ts +++ b/test/gasEstimator/gasEstimator.test.ts @@ -3,7 +3,12 @@ import chaiAsPromised from 'chai-as-promised'; import sinonChai from 'sinon-chai'; import { createSandbox, SinonStubbedInstance } from 'sinon'; import { BigNumber, constants, providers, Wallet } from 'ethers'; -import { RelayHub, RelayHub__factory } from '@rsksmart/rif-relay-contracts'; +import { + BaseSmartWalletFactory__factory, + RelayHub, + RelayHub__factory, + SmartWalletFactory, +} from '@rsksmart/rif-relay-contracts'; import { FAKE_DEPLOY_REQUEST, @@ -19,6 +24,7 @@ import { PRE_RELAY_GAS_COST, resolveSmartWalletAddress, standardMaxPossibleGasEstimation, + isAccountCreated, } from '../../src/gasEstimator/utils'; import { createRandomAddress } from '../utils'; import type { EnvelopingTxRequest } from '../../src'; @@ -47,6 +53,7 @@ describe('GasEstimator', function () { request: { ...FAKE_RELAY_TRANSACTION_REQUEST.relayRequest.request, tokenAmount: 0, + nonce: constants.Zero, }, }, }; @@ -57,6 +64,7 @@ describe('GasEstimator', function () { request: { ...FAKE_DEPLOY_TRANSACTION_REQUEST.relayRequest.request, tokenAmount: 0, + nonce: constants.Zero, }, }, }; @@ -300,7 +308,14 @@ describe('GasEstimator', function () { }, } as unknown as RelayHub; + const factoryStub = { + nonce: () => Promise.resolve(constants.One), + } as unknown as SmartWalletFactory; + sandbox.stub(RelayHub__factory, 'connect').returns(relayHubStub); + sandbox + .stub(BaseSmartWalletFactory__factory, 'connect') + .returns(factoryStub); }); describe('with contract execution', function () { @@ -314,7 +329,7 @@ describe('GasEstimator', function () { .add(fakeTokenGas) .add(fakeInternalGas) .add(gasEstimatorUtils.POST_RELAY_DEPLOY_GAS_COST) - .add(gasEstimatorUtils.POST_DEPLOY_EXECUTION_FACTOR * 3); + .add(gasEstimatorUtils.POST_DEPLOY_EXECUTION); expect(estimation).eqls( expectedEstimation, @@ -338,7 +353,7 @@ describe('GasEstimator', function () { .add(fakeTokenGas) .add(fakeInternalGas) .add(gasEstimatorUtils.POST_RELAY_DEPLOY_GAS_COST) - .add(gasEstimatorUtils.POST_DEPLOY_EXECUTION_FACTOR * 3); + .add(gasEstimatorUtils.POST_DEPLOY_EXECUTION); expect(estimation).eqls( expectedEstimation, @@ -370,7 +385,7 @@ describe('GasEstimator', function () { const expectedEstimation = deployEstimation .add(fakeTokenGas) .add(gasEstimatorUtils.POST_RELAY_DEPLOY_GAS_COST) - .add(gasEstimatorUtils.POST_DEPLOY_EXECUTION_FACTOR * 8); + .add(gasEstimatorUtils.POST_DEPLOY_NO_EXECUTION); expect(estimation).eqls( expectedEstimation, @@ -393,7 +408,7 @@ describe('GasEstimator', function () { const expectedEstimation = deployEstimation .add(fakeTokenGas) .add(gasEstimatorUtils.POST_RELAY_DEPLOY_GAS_COST) - .add(gasEstimatorUtils.POST_DEPLOY_EXECUTION_FACTOR * 8); + .add(gasEstimatorUtils.POST_DEPLOY_NO_EXECUTION); expect(estimation).eqls( expectedEstimation, @@ -431,4 +446,40 @@ describe('GasEstimator', function () { expect(smartWalletAddress).to.be.equal(expectedSmartWalletAddress); }); }); + + describe('isAccountCreated', function () { + let providerStub: SinonStubbedInstance; + + beforeEach(function () { + providerStub = sandbox.createStubInstance(providers.BaseProvider); + sandbox.stub(clientConfiguration, 'getProvider').returns(providerStub); + providerStub.getBalance.resolves(BigNumber.from(1)); + providerStub.getTransactionCount.resolves(1); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should return true if balance is not zero', async function () { + const touched = await isAccountCreated(createRandomAddress()); + + expect(touched).to.be.true; + }); + + it('should return true if transaction count is greater than 0', async function () { + const touched = await isAccountCreated(createRandomAddress()); + + expect(touched).to.be.true; + }); + + it('should return false if balance is zero and transaction count is 0', async function () { + providerStub.getBalance.resolves(BigNumber.from(0)); + providerStub.getTransactionCount.resolves(0); + + const touched = await isAccountCreated(createRandomAddress()); + + expect(touched).to.be.false; + }); + }); });