Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
DataSource,
SetDataSources,
} from "../governance_payload/SetDataSources";
import { SetTransactionFee } from "../governance_payload/SetTransactionFee";

test("GovernancePayload ser/de", (done) => {
jest.setTimeout(60000);
Expand Down Expand Up @@ -424,6 +425,12 @@ function governanceActionArb(): Arbitrary<PythGovernanceAction> {
Buffer.from(token),
);
});
} else if (header.action === "SetTransactionFee") {
return fc
.record({ v: fc.bigUintN(64), e: fc.bigUintN(64) })
.map(({ v, e }) => {
return new SetTransactionFee(header.targetChainId, v, e);
});
} else {
throw new Error("Unsupported action type");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const TargetAction = {
RequestGovernanceDataSourceTransfer: 5,
SetWormholeAddress: 6,
SetFeeInToken: 7,
SetTransactionFee: 8,
} as const;

export const EvmExecutorAction = {
Expand Down Expand Up @@ -46,6 +47,8 @@ export function toActionName(
return "SetWormholeAddress";
case 7:
return "SetFeeInToken";
case 8:
return "SetTransactionFee";
}
} else if (
deserialized.moduleId == MODULE_EVM_EXECUTOR &&
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { PythGovernanceActionImpl } from "./PythGovernanceAction";
import * as BufferLayout from "@solana/buffer-layout";
import * as BufferLayoutExt from "./BufferLayoutExt";
import { ChainName } from "../chains";

/** Set the transaction fee on the target chain to newFeeValue * 10^newFeeExpo */
export class SetTransactionFee extends PythGovernanceActionImpl {
static layout: BufferLayout.Structure<
Readonly<{ newFeeValue: bigint; newFeeExpo: bigint }>
> = BufferLayout.struct([
BufferLayoutExt.u64be("newFeeValue"),
BufferLayoutExt.u64be("newFeeExpo"),
]);

constructor(
targetChainId: ChainName,
readonly newFeeValue: bigint,
readonly newFeeExpo: bigint,
) {
super(targetChainId, "SetTransactionFee");
}

static decode(data: Buffer): SetTransactionFee | undefined {
const decoded = PythGovernanceActionImpl.decodeWithPayload(
data,
"SetTransactionFee",
SetTransactionFee.layout,
);
if (!decoded) return undefined;

return new SetTransactionFee(
decoded[0].targetChainId,
decoded[1].newFeeValue,
decoded[1].newFeeExpo,
);
}

encode(): Buffer {
return super.encodeWithPayload(SetTransactionFee.layout, {
newFeeValue: this.newFeeValue,
newFeeExpo: this.newFeeExpo,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
StarknetSetWormholeAddress,
} from "./SetWormholeAddress";
import { EvmExecute } from "./ExecuteAction";
import { SetTransactionFee } from "./SetTransactionFee";

/** Decode a governance payload */
export function decodeGovernancePayload(
Expand Down Expand Up @@ -73,6 +74,8 @@ export function decodeGovernancePayload(
}
case "Execute":
return EvmExecute.decode(data);
case "SetTransactionFee":
return SetTransactionFee.decode(data);
default:
return undefined;
}
Expand All @@ -86,5 +89,6 @@ export * from "./GovernanceDataSourceTransfer";
export * from "./SetDataSources";
export * from "./SetValidPeriod";
export * from "./SetFee";
export * from "./SetTransactionFee";
export * from "./SetWormholeAddress";
export * from "./ExecuteAction";
3 changes: 2 additions & 1 deletion target_chains/ethereum/contracts/contracts/pyth/Pyth.sol
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,8 @@ abstract contract Pyth is
function getTotalFee(
uint totalNumUpdates
) private view returns (uint requiredFee) {
return totalNumUpdates * singleUpdateFeeInWei();
return
(totalNumUpdates * singleUpdateFeeInWei()) + transactionFeeInWei();
}

function findIndexOfPriceId(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,8 @@ contract PythGetters is PythState {
function governanceDataSourceIndex() public view returns (uint32) {
return _state.governanceDataSourceIndex;
}

function transactionFeeInWei() public view returns (uint) {
return _state.transactionFeeInWei;
}
}
12 changes: 12 additions & 0 deletions target_chains/ethereum/contracts/contracts/pyth/PythGovernance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ abstract contract PythGovernance is
address oldWormholeAddress,
address newWormholeAddress
);
event TransactionFeeSet(uint oldFee, uint newFee);

function verifyGovernanceVM(
bytes memory encodedVM
Expand Down Expand Up @@ -97,6 +98,8 @@ abstract contract PythGovernance is
parseSetWormholeAddressPayload(gi.payload),
encodedVM
);
} else if (gi.action == GovernanceAction.SetTransactionFee) {
setTransactionFee(parseSetTransactionFeePayload(gi.payload));
} else {
revert PythErrors.InvalidGovernanceMessage();
}
Expand Down Expand Up @@ -243,4 +246,13 @@ abstract contract PythGovernance is

emit WormholeAddressSet(oldWormholeAddress, address(wormhole()));
}

function setTransactionFee(
SetTransactionFeePayload memory payload
) internal {
uint oldFee = transactionFeeInWei();
setTransactionFeeInWei(payload.newFee);

emit TransactionFeeSet(oldFee, transactionFeeInWei());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ contract PythGovernanceInstructions {
SetFee, // 3
SetValidPeriod, // 4
RequestGovernanceDataSourceTransfer, // 5
SetWormholeAddress // 6
SetWormholeAddress, // 6
SetTransactionFee // 7
}

struct GovernanceInstruction {
Expand Down Expand Up @@ -77,6 +78,10 @@ contract PythGovernanceInstructions {
address newWormholeAddress;
}

struct SetTransactionFeePayload {
uint newFee;
}

/// @dev Parse a GovernanceInstruction
function parseGovernanceInstruction(
bytes memory encodedInstruction
Expand Down Expand Up @@ -220,4 +225,22 @@ contract PythGovernanceInstructions {
if (encodedPayload.length != index)
revert PythErrors.InvalidGovernanceMessage();
}

/// @dev Parse a SetTransactionFeePayload (action 7) with minimal validation
function parseSetTransactionFeePayload(
bytes memory encodedPayload
) public pure returns (SetTransactionFeePayload memory stf) {
uint index = 0;

uint64 val = encodedPayload.toUint64(index);
index += 8;

uint64 expo = encodedPayload.toUint64(index);
index += 8;

stf.newFee = uint256(val) * uint256(10) ** uint256(expo);

if (encodedPayload.length != index)
revert PythErrors.InvalidGovernanceMessage();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,8 @@ contract PythSetters is PythState, IPythEvents {
function setGovernanceDataSourceIndex(uint32 newIndex) internal {
_state.governanceDataSourceIndex = newIndex;
}

function setTransactionFeeInWei(uint fee) internal {
_state.transactionFeeInWei = fee;
}
}
2 changes: 2 additions & 0 deletions target_chains/ethereum/contracts/contracts/pyth/PythState.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ contract PythStorage {
// Mapping of cached price information
// priceId => PriceInfo
mapping(bytes32 => PythInternalStructs.PriceInfo) latestPriceInfo;
// Fee charged per transaction, in addition to per-update fees
uint transactionFeeInWei;
}
}

Expand Down
Loading
Loading