Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for cip 66 transactions #23

Merged
merged 17 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"build": "yarn build:main && yarn build:module",
"build:main": "tsc -p tsconfig.json",
"build:module": "tsc -p tsconfig.module.json",
"test": "jest ./tests"
"test": "jest"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed and means that calling yarn test does not work

},
"engines": {
"node": ">=18.14.2"
Expand Down
10 changes: 10 additions & 0 deletions src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,13 @@ export const EIGHT = 8;

// NOTE: Logic stolen from https://github.com/celo-org/celo-monorepo/blob/e7ebc92cb0715dc56c9d7f613dca81e076541cf3/packages/sdk/connect/src/connection.ts#L382-L396
export const GAS_INFLATION_FACTOR = 130n;


/*
* If a contract is deployed to this address then Celo has transitioned to a Layer 2
* https://github.com/celo-org/celo-monorepo/blob/da9b4955c1fdc8631980dc4adf9b05e0524fc228/packages/protocol/contracts-0.8/common/IsL2Check.sol#L17
*/
export const L2_PROXY_ADMIN_ADDRESS = '0x4200000000000000000000000000000000000018'


export const CELO_REGISTRY_ADDRESS = '0x000000000000000000000000000000000000ce10'
6 changes: 3 additions & 3 deletions src/lib/CeloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ export default class CeloProvider extends JsonRpcProvider {
"%response"
);
}

async getFeeData(feeCurrency?: string): Promise<FeeData> {
if (!feeCurrency) {
// for eip1559 and cip66 transactions are denominated in CELO, cip64 fees must be looked up in the fee token
async getFeeData(feeCurrency?: string, denominateInCelo?: boolean): Promise<FeeData> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What purpose does this new param serve? When would denominate be true but fee currency would be truthy?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because with this new transaction type feeCurrency exists but intead of maxFeePerGas and maxPriorityFeePerGas needing be looked up in that in the price for that token they are instead priced in CELO.

if (!feeCurrency || denominateInCelo) {
return super.getFeeData();
}
// On Celo, `eth_gasPrice` returns the base fee for the given currency multiplied 2
Expand Down
72 changes: 63 additions & 9 deletions src/lib/CeloWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import {
Wordlist,
} from "ethers";
import CeloProvider from "./CeloProvider";
import { adjustForGasInflation, isEmpty } from "./transaction/utils";
import { adjustForGasInflation, convertFromCeloToToken, isEmpty } from "./transaction/utils";
import { CeloTransaction, CeloTransactionRequest, serializeCeloTransaction } from "./transactions";
import { L2_PROXY_ADMIN_ADDRESS } from "../consts";
import { getConversionRateFromCeloToToken } from "./CoreContract";

const forwardErrors = [
"INSUFFICIENT_FUNDS",
Expand All @@ -23,6 +25,16 @@ const forwardErrors = [
] as ErrorCode[];

export default class CeloWallet extends Wallet {

async isCel2(){
aaronmgdr marked this conversation as resolved.
Show resolved Hide resolved
const code = await this.provider?.getCode(L2_PROXY_ADMIN_ADDRESS)
if (typeof code === 'string') {
return code != '0x' && code.length > 2
}
return false
}


/**
* Override to skip checkTransaction step which rejects Celo tx properties
* https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-signer/src.ts/index.ts
Expand Down Expand Up @@ -57,13 +69,7 @@ export default class CeloWallet extends Wallet {
}
}

if (isEmpty(tx.maxPriorityFeePerGas) || isEmpty(tx.maxFeePerGas)) {
const { maxFeePerGas, maxPriorityFeePerGas } = (await (
this.provider as CeloProvider
)?.getFeeData(tx.feeCurrency as string | undefined))!;
tx.maxFeePerGas = maxFeePerGas;
tx.maxPriorityFeePerGas = maxPriorityFeePerGas;
}
await this.populateFees(tx);

if (isEmpty(tx.chainId)) {
tx.chainId = (await this.provider!.getNetwork()).chainId;
Expand All @@ -80,6 +86,33 @@ export default class CeloWallet extends Wallet {
return resolveProperties<CeloTransaction>(tx);
}

// sets feedata for the transaction.
//
async populateFees(tx: CeloTransactionRequest) {
const isCel2 = await this.isCel2()
const noFeeCurrency = !tx.feeCurrency
const useCIP66ForEasyFeeTransactions = isCel2 && !noFeeCurrency
// CIP 66 transactions are denominated in CELO not the fee token
const feesAreInCELO = noFeeCurrency || useCIP66ForEasyFeeTransactions

if (isEmpty(tx.maxPriorityFeePerGas) || isEmpty(tx.maxFeePerGas)) {
const { maxFeePerGas, maxPriorityFeePerGas } = (await (
this.provider as CeloProvider
)?.getFeeData(tx.feeCurrency, feesAreInCELO))!;

tx.maxFeePerGas = maxFeePerGas;
tx.maxPriorityFeePerGas = maxPriorityFeePerGas;

if (useCIP66ForEasyFeeTransactions && isEmpty(tx.maxFeeInFeeCurrency)) {
const gasLimit = BigInt(tx.gasLimit!)
const maxFeeInFeeCurrency = await this.estimateMaxFeeInFeeToken({feeCurrency: tx.feeCurrency!, gasLimit, maxFeePerGas: maxFeePerGas!})
tx.maxFeeInFeeCurrency = maxFeeInFeeCurrency
}
}

return tx
}

/**
* Override to serialize transaction using custom serialize method
* https://github.com/ethers-io/ethers.js/blob/master/packages/wallet/src.ts/index.ts
Expand Down Expand Up @@ -124,7 +157,28 @@ export default class CeloWallet extends Wallet {
}

/**
* Override to support alternative gas currencies
* For cip 66 transactions (the prefered way to pay for gas with fee tokens on Cel2) it is necessary
* to provide the absolute limit one is willing to pay denominated in the token.
* In contrast with earlier tx types for fee currencies (celo legacy, cip42, cip 64).
*
* Calulating Estimation requires the gas, maxfeePerGas and the conversion rate from CELO to feeToken
* https://github.com/celo-org/celo-proposals/blob/master/CIPs/cip-0066.md
*/
async estimateMaxFeeInFeeToken({gasLimit, maxFeePerGas, feeCurrency}: {gasLimit: bigint, maxFeePerGas: bigint, feeCurrency: string}) {
const maxGasFeesInCELO = gasLimit * maxFeePerGas
const [numerator, denominator] = await getConversionRateFromCeloToToken(feeCurrency, {wallet: this})
const feeDenominatedInToken = convertFromCeloToToken({
amountInCelo: maxGasFeesInCELO,
ratioTOKEN: numerator,
ratioCELO: denominator
})

return feeDenominatedInToken
}

/**
* Override to support alternative gas currencies
* @dev (for cip66 txn you want gasPrice in CELO so dont pass in the feeToken)
* https://github.com/celo-tools/ethers.js/blob/master/packages/abstract-signer/src.ts/index.ts
*/
async getGasPrice(feeCurrencyAddress?: string): Promise<bigint> {
Expand Down
72 changes: 72 additions & 0 deletions src/lib/CoreContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {Contract} from "ethers"
import CeloWallet from "./CeloWallet"
import { CELO_REGISTRY_ADDRESS } from "../consts"

const MINIMAL_ORACLE_INTERFACE = [
{
"constant": true,
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "medianRate",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]


const MINIMAL_REGISTRY_ABI = [{
"constant": true,
"inputs": [
{
"internalType": "string",
"name": "identifier",
"type": "string"
}
],
"name": "getAddressForString",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}]



export async function getConversionRateFromCeloToToken(tokenAddress: string, {wallet}: {wallet: CeloWallet}): Promise<[bigint, bigint]> {
aaronmgdr marked this conversation as resolved.
Show resolved Hide resolved
const registry = new Contract(CELO_REGISTRY_ADDRESS, MINIMAL_REGISTRY_ABI , wallet)
aaronmgdr marked this conversation as resolved.
Show resolved Hide resolved

const oracleAddress = await registry.getAddressForString('SortedOracles')

const oracle = new Contract(oracleAddress, MINIMAL_ORACLE_INTERFACE, wallet)

const [numerator, denominator]: bigint[] = await oracle.medianRate(tokenAddress)
// The function docs for the Contract are confusing but in ContractKit the Sorted orcles wrapper
// defines numerator as the amount of the token and denominiator as equvalent value in CELO
// https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/contractkit/src/wrappers/SortedOracles.ts#L80
// https://github.com/celo-org/celo-monorepo/blob/master/packages/protocol/contracts/stability/SortedOracles.sol
return [numerator, denominator]
}
29 changes: 27 additions & 2 deletions src/lib/transaction/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { hexlify, BigNumberish, isBytesLike, toBeHex } from "ethers";
import { GAS_INFLATION_FACTOR } from "../../consts";
import { CeloTransaction, CeloTransactionCip64, CeloTransactionCip66 } from "../transactions";

export function isEmpty(value: string | BigNumberish | undefined | null) {
if (value === undefined || value === null || value === "0" || value === 0n) {
Expand All @@ -15,8 +16,14 @@ export function isPresent(value: string | BigNumberish | undefined | null) {
return !isEmpty(value);
}

export function isCIP64(tx: any) {
return isPresent(tx.feeCurrency);
export function isCIP64(tx: CeloTransaction): tx is CeloTransactionCip64 {
return isPresent((tx as CeloTransactionCip64).feeCurrency) &&
isEmpty((tx as CeloTransactionCip66).maxFeeInFeeCurrency);
}

export function isCIP66(tx: CeloTransaction): tx is CeloTransactionCip66 {
return isPresent((tx as CeloTransactionCip66).feeCurrency)
&& isPresent((tx as CeloTransactionCip66).maxFeeInFeeCurrency);
}

export function concatHex(values: string[]): `0x${string}` {
Expand All @@ -41,3 +48,21 @@ export function adjustForGasInflation(gas: bigint): bigint {
// NOTE: prevent floating point math
return (gas * GAS_INFLATION_FACTOR) / 100n;
}

interface ConversionParams {
amountInCelo: bigint,
ratioCELO: bigint,
ratioTOKEN: bigint
}
/**
*
* @param param0 @ConversionParams
* ratioTOKEN will come from the first position (or numerator) of tuple returned from SortedOracles.medianRate
* ratioCELO will come from the second position (or denominator) of tuple returned from SortedOracles.medianRate

* @returns amount in token equal in value to the amountInCelo given.
*/
export function convertFromCeloToToken({amountInCelo, ratioCELO, ratioTOKEN }: ConversionParams) {
return amountInCelo * ratioCELO / ratioTOKEN
}

Loading
Loading