Skip to content

Commit

Permalink
feat: add ComposableBridging simulation unit tests (#472)
Browse files Browse the repository at this point in the history
Signed-off-by: james-a-morris <[email protected]>
  • Loading branch information
james-a-morris authored Jan 16, 2024
1 parent 674de8c commit 1ba4398
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 9 deletions.
27 changes: 27 additions & 0 deletions contracts/MockAcrossMessageContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

// This interface is expected to be implemented by any contract
// that expects to receive messages from the SpokePool.
// Retrieved from https://github.com/across-protocol/contracts-v2/blob/master/contracts/SpokePool.sol
interface AcrossMessageHandler {
function handleAcrossMessage(
address tokenSent,
uint256 amount,
bool fillCompleted,
address relayer,
bytes memory message
) external;
}

contract MockAcrossMessageContract is AcrossMessageHandler {
function handleAcrossMessage(address, uint256, bool, address, bytes memory message) external virtual override {
// For the case that we want to test a revert.
require(keccak256(message) != keccak256(bytes("REVERT")), "MockAcrossMessageContract: revert");

// Iterate from 0 to 1000 to simulate a long-running operation.
for (uint256 i = 0; i < 1000; i++) {
// Do a bit of work.
}
}
}
7 changes: 4 additions & 3 deletions src/relayFeeCalculator/relayFeeCalculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
getTokenInformationFromAddress,
TransactionCostEstimate,
} from "../utils";
import { DEFAULT_SIMULATED_RELAYER_ADDRESS } from "../constants";
import { DEFAULT_SIMULATED_RELAYER_ADDRESS, TOKEN_SYMBOLS_MAP } from "../constants";
import { Deposit } from "../interfaces";

// This needs to be implemented for every chain and passed into RelayFeeCalculator
Expand Down Expand Up @@ -219,9 +219,10 @@ export class RelayFeeCalculator {
amountToRelay: BigNumberish,
simulateZeroFill = false,
relayerAddress = DEFAULT_SIMULATED_RELAYER_ADDRESS,
_tokenPrice?: number
_tokenPrice?: number,
tokenMapping = TOKEN_SYMBOLS_MAP
): Promise<BigNumber> {
const token = getTokenInformationFromAddress(deposit.originToken);
const token = getTokenInformationFromAddress(deposit.originToken, tokenMapping);
if (!isDefined(token)) {
throw new Error(`Could not find token information for ${deposit.originToken}`);
}
Expand Down
13 changes: 9 additions & 4 deletions src/utils/TokenUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,21 @@ export function resolveSymbolOnChain(chainId: number, symbol: string): L1Token {
* Returns the contract address for a given token symbol and chainId.
* @param symbol A case-insensitive token symbol.
* @param chainId The chainId to resolve the contract address for.
* @param tokenMapping A parameter to determine where to source token information. Defaults to the constants-v2 variant.
* @returns The contract address for the given token symbol and chainId, or undefined if the token symbol is not supported.
*/
export const resolveContractFromSymbol = (symbol: string, chainId: string): string | undefined => {
return Object.values(TOKEN_SYMBOLS_MAP).find((details) => {
export const resolveContractFromSymbol = (
symbol: string,
chainId: string,
tokenMapping = TOKEN_SYMBOLS_MAP
): string | undefined => {
return Object.values(tokenMapping).find((details) => {
return details.symbol.toLowerCase() === symbol.toLowerCase();
})?.addresses[Number(chainId)];
};

export function getTokenInformationFromAddress(address: string): L1Token | undefined {
const details = Object.values(TOKEN_SYMBOLS_MAP).find((details) => {
export function getTokenInformationFromAddress(address: string, tokenMapping = TOKEN_SYMBOLS_MAP): L1Token | undefined {
const details = Object.values(tokenMapping).find((details) => {
return Object.values(details.addresses).some((t) => t.toLowerCase() === address.toLowerCase());
});
return isDefined(details)
Expand Down
146 changes: 144 additions & 2 deletions test/relayFeeCalculator.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
import dotenv from "dotenv";
import hre from "hardhat";
import { RelayFeeCalculator, QueryInterface } from "../src/relayFeeCalculator/relayFeeCalculator";
import { toBNWei, toBN, toGWei, TransactionCostEstimate } from "../src/utils";
import { assert, buildDepositForRelayerFeeTest, expect, randomAddress } from "./utils";
import { toBNWei, toBN, toGWei, TransactionCostEstimate, bnOne, bnZero, bnUint256Max } from "../src/utils";
import {
BigNumber,
Contract,
SignerWithAddress,
assert,
assertPromiseError,
assertPromisePasses,
buildDepositForRelayerFeeTest,
deploySpokePoolWithToken,
ethers,
expect,
getContractFactory,
randomAddress,
setupTokensForWallet,
} from "./utils";
import { TOKEN_SYMBOLS_MAP } from "@across-protocol/constants-v2";
import { EthereumQueries } from "../src/relayFeeCalculator";
import { EMPTY_MESSAGE } from "../src/constants";
import { SpokePool } from "@across-protocol/contracts-v2";

dotenv.config({ path: ".env" });

Expand Down Expand Up @@ -272,3 +291,126 @@ describe("RelayFeeCalculator", () => {
assert.equal(client.capitalFeePercent("0", "WBTC").toString(), Number.MAX_SAFE_INTEGER.toString());
});
});

describe("RelayFeeCalculator: Composable Bridging", function () {
let spokePool: SpokePool, erc20: Contract, destErc20: Contract, weth: Contract;
let client: RelayFeeCalculator;
let queries: EthereumQueries;
let testContract: Contract;
let owner: SignerWithAddress, relayer: SignerWithAddress, depositor: SignerWithAddress;
let tokenMap: typeof TOKEN_SYMBOLS_MAP;
let testGasFeePct: (message?: string) => Promise<BigNumber>;

beforeEach(async function () {
[owner, relayer, depositor] = await ethers.getSigners();

const {
spokePool: _spokePool,
erc20: _erc20,
weth: _weth,
destErc20: _destErc20,
} = await deploySpokePoolWithToken(1, 10);

spokePool = _spokePool as SpokePool;
erc20 = _erc20;
weth = _weth;
destErc20 = _destErc20;

tokenMap = {
USDC: {
name: "USDC",
symbol: "USDC",
decimals: 6,
addresses: {
1: erc20.address,
10: erc20.address,
},
},
} as unknown as typeof TOKEN_SYMBOLS_MAP;
await (spokePool as Contract).setChainId(10); // The spoke pool for a fill should be at the destinationChainId.
await setupTokensForWallet(spokePool, relayer, [erc20, destErc20], weth, 100);

testContract = await hre["upgrades"].deployProxy(await getContractFactory("MockAcrossMessageContract", owner), []);
queries = new EthereumQueries(spokePool.provider, tokenMap, spokePool.address, relayer.address);
client = new RelayFeeCalculator({ queries, capitalCostsConfig: testCapitalCostsConfig });

testGasFeePct = (message?: string) =>
client.gasFeePercent(
{
amount: bnOne,
quoteTimestamp: 1,
recipient: testContract.address,
relayerFeePct: bnZero,
depositId: 1000000,
depositor: depositor.address,
originChainId: 1,
destinationChainId: 10,
originToken: erc20.address,
destinationToken: erc20.address,
message: message || EMPTY_MESSAGE,
realizedLpFeePct: bnZero,
},
1,
false,
relayer.address,
1,
tokenMap
);
});
it("should not revert if no message is passed", async () => {
await assertPromisePasses(testGasFeePct());
});
it("should revert if the contract message fails", async () => {
// Per our test contract, this message will revert.
const message = ethers.utils.hexlify(ethers.utils.toUtf8Bytes("REVERT"));
await assertPromiseError(testGasFeePct(message), "MockAcrossMessageContract: revert");
});
it.only("should be more gas to call a contract with a message", async () => {
const gasFeeFromTestContract = await testContract.estimateGas.handleAcrossMessage(
erc20.address,
bnOne,
true,
relayer.address,
"0x04"
);
const gasFeeFromFillRelayWithoutMessage = await spokePool.estimateGas.fillRelay(
depositor.address,
testContract.address,
erc20.address,
1,
1,
10,
1,
bnOne,
bnOne,
3_000_000,
EMPTY_MESSAGE,
bnUint256Max
);
const gasFeeFromFillRelayWithMessage = await spokePool.estimateGas.fillRelay(
depositor.address,
testContract.address,
erc20.address,
1,
1,
10,
1,
bnOne,
bnOne,
3_000_000,
"0x04",
bnUint256Max
);
const intrinsicGasCost = toBN(21_000);

// We expect the gas fee to be higher when calling a contract with a message
// Specifically, we expect that our gas should be larger than a call to the test contract
// and a call to the fillRelay function without a message.
// We should account for the second intrinsic gas cost when adding the gas estimation from *both* calls.
const gasFeeEstimatedByCallingContract = gasFeeFromFillRelayWithoutMessage
.add(gasFeeFromTestContract)
.sub(intrinsicGasCost);

expect(gasFeeFromFillRelayWithMessage.gt(gasFeeEstimatedByCallingContract)).to.be.true;
});
});

0 comments on commit 1ba4398

Please sign in to comment.