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

Fix/chainlight remediations #81

Merged
merged 13 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all 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: 2 additions & 0 deletions contracts/interfaces/paymasters/IBiconomyTokenPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ interface IBiconomyTokenPaymaster {
*/
event FeeReceiverChanged(address indexed _oldfeeReceiver, address indexed _newfeeReceiver, address indexed _actor);

event TokensWithdrawn(address indexed _token, address indexed _to, uint256 indexed _amount, address actor);
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure _amount should be indexed

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we can index 3 params and I think it's fine. also out of scope of remediations PR


/**
* Designed to enable tracking how much fees were charged from the sender and in which ERC20 token
* More information can be emitted like exchangeRate used, what was the source of exchangeRate etc
Expand Down
10 changes: 5 additions & 5 deletions contracts/libs/MathLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ pragma solidity ^0.8.23;
library MathLib {
function minuint256(uint256 a, uint256 b) internal pure returns (uint256 result) {
assembly {
result := xor(b, mul(xor(b, a), gt(a, b)))
}
result := xor(b, mul(xor(b, a), gt(b, a)))
}
}

function maxuint256(uint256 a, uint256 b) internal pure returns (uint256 result) {
assembly {
result := xor(a, mul(xor(a, b), gt(b, a)))
}
}

function minuint32(uint32 a, uint32 b) internal pure returns (uint32 result) {
function minuint32(uint32 a, uint32 b) internal pure returns (uint32 result) {
assembly {
result := xor(b, mul(xor(b, a), gt(a, b)))
}
result := xor(b, mul(xor(b, a), gt(b, a)))
}
}

function maxuint32(uint32 a, uint32 b) internal pure returns (uint32 result) {
assembly {
Expand Down
179 changes: 106 additions & 73 deletions contracts/token/BiconomyTokenPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@account-abstraction/contracts/core/Helpers.sol" as Helpers;
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import "../utils/SafeTransferLib.sol";
import {MathLib} from "../libs/MathLib.sol";
import {TokenPaymasterErrors} from "./TokenPaymasterErrors.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import {OracleAggregator} from "./oracles/OracleAggregator.sol";
Expand Down Expand Up @@ -79,6 +80,10 @@ contract BiconomyTokenPaymaster is
}
}

receive() external payable {
emit Received(msg.sender, msg.value);
}

/**
* @dev Set a new verifying signer address.
* Can only be called by the owner of the contract.
Expand Down Expand Up @@ -180,11 +185,12 @@ contract BiconomyTokenPaymaster is
onlyOwner
nonReentrant
{
if (token.length != amount.length) {
uint256 tokLen = token.length;
if (tokLen != amount.length) {
revert TokensAndAmountsLengthMismatch();
}
unchecked {
for (uint256 i; i < token.length;) {
for (uint256 i; i < tokLen;) {
_withdrawERC20(token[i], target, amount[i]);
++i;
}
Expand All @@ -198,7 +204,8 @@ contract BiconomyTokenPaymaster is
*/
function withdrawMultipleERC20Full(IERC20[] calldata token, address target) public payable onlyOwner nonReentrant {
unchecked {
for (uint256 i; i < token.length;) {
uint256 tokLen = token.length;
for (uint256 i; i < tokLen;) {
uint256 amount = token[i].balanceOf(address(this));
_withdrawERC20(token[i], target, amount);
++i;
Expand Down Expand Up @@ -285,71 +292,6 @@ contract BiconomyTokenPaymaster is
signature = paymasterAndData[VALID_PND_OFFSET + 53:];
}

function _getRequiredPrefund(UserOperation calldata userOp) internal view returns (uint256 requiredPrefund) {
unchecked {
uint256 requiredGas =
userOp.callGasLimit + userOp.verificationGasLimit + userOp.preVerificationGas + unaccountedEPGasOverhead;

requiredPrefund = requiredGas * userOp.maxFeePerGas;
}
}

/**
* @dev Verify that an external signer signed the paymaster data of a user operation.
* The paymaster data is expected to be the paymaster address, request data and a signature over the entire request parameters.
* paymasterAndData: hexConcat([paymasterAddress, priceSource, abi.encode(validUntil, validAfter, feeToken, exchangeRate, priceMarkup), signature])
* @param userOp The UserOperation struct that represents the current user operation.
* userOpHash The hash of the UserOperation struct.
* @param requiredPreFund The required amount of pre-funding for the paymaster.
* @return context A context string returned by the entry point after successful validation.
* @return validationData An integer returned by the entry point after successful validation.
*/
function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 requiredPreFund)
internal
view
override
returns (bytes memory context, uint256 validationData)
{
(requiredPreFund);

(
ExchangeRateSource priceSource,
uint48 validUntil,
uint48 validAfter,
address feeToken,
uint128 exchangeRate,
uint32 priceMarkup,
bytes calldata signature
) = parsePaymasterAndData(userOp.paymasterAndData);

bytes32 _hash = getHash(userOp, priceSource, validUntil, validAfter, feeToken, exchangeRate, priceMarkup)
.toEthSignedMessageHash();

//don't revert on signature failure: return SIG_VALIDATION_FAILED
if (verifyingSigner != _hash.recover(signature)) {
// empty context and sigFailed true
return (context, Helpers._packValidationData(true, validUntil, validAfter));
}

address account = userOp.getSender();

// This model assumes irrespective of priceSource exchangeRate is always sent from outside
// for below checks you would either need maxCost or some exchangeRate

uint256 btpmRequiredPrefund = _getRequiredPrefund(userOp);

uint256 tokenRequiredPreFund = (btpmRequiredPrefund * exchangeRate) / 10 ** 18;
require(priceMarkup <= 2e6, "BTPM: price markup percentage too high");
require(
IERC20(feeToken).balanceOf(account) >= ((tokenRequiredPreFund * priceMarkup) / PRICE_DENOMINATOR),
"BTPM: account does not have enough token balance"
);

context = abi.encode(account, feeToken, priceSource, exchangeRate, priceMarkup, userOpHash);

return (context, Helpers._packValidationData(false, validUntil, validAfter));
}

/**
* @dev Executes the paymaster's payment conditions
* @param mode tells whether the op succeeded, reverted, or if the op succeeded but cause the postOp to revert
Expand All @@ -362,6 +304,8 @@ contract BiconomyTokenPaymaster is
ExchangeRateSource priceSource;
uint128 exchangeRate;
uint32 priceMarkup;
uint256 maxFeePerGas;
uint256 maxPriorityFeePerGas;
bytes32 userOpHash;
assembly ("memory-safe") {
let offset := context.offset
Expand All @@ -381,6 +325,12 @@ contract BiconomyTokenPaymaster is
priceMarkup := calldataload(offset)
offset := add(offset, 0x20)

maxFeePerGas := calldataload(offset)
offset := add(offset, 0x20)

maxPriorityFeePerGas := calldataload(offset)
offset := add(offset, 0x20)

userOpHash := calldataload(offset)
}

Expand All @@ -391,11 +341,16 @@ contract BiconomyTokenPaymaster is
if (result != 0) effectiveExchangeRate = result;
}

uint256 effectiveGasPrice = getGasPrice(
maxFeePerGas,
maxPriorityFeePerGas
);

// We could either touch the state for BASEFEE and calculate based on maxPriorityFee passed (to be added in context along with maxFeePerGas) or just use tx.gasprice
uint256 charge; // Final amount to be charged from user account
{
uint256 actualTokenCost =
((actualGasCost + (unaccountedEPGasOverhead * tx.gasprice)) * effectiveExchangeRate) / 1e18;
((actualGasCost + (unaccountedEPGasOverhead * effectiveGasPrice)) * effectiveExchangeRate) / 1e18;
charge = ((actualTokenCost * priceMarkup) / PRICE_DENOMINATOR);
}

Expand Down Expand Up @@ -425,12 +380,90 @@ contract BiconomyTokenPaymaster is
}
}

function _getRequiredPrefund(UserOperation calldata userOp) internal view returns (uint256 requiredPrefund) {
unchecked {
uint256 requiredGas =
userOp.callGasLimit + userOp.verificationGasLimit + userOp.preVerificationGas + unaccountedEPGasOverhead;

requiredPrefund = requiredGas * userOp.maxFeePerGas;
}
}

// Note: do not use this in validation phase
function getGasPrice(
uint256 maxFeePerGas,
uint256 maxPriorityFeePerGas
) internal view returns (uint256) {
if (maxFeePerGas == maxPriorityFeePerGas) {
//legacy mode (for networks that don't support basefee opcode)
return maxFeePerGas;
}
return
MathLib.minuint256(
maxFeePerGas,
maxPriorityFeePerGas + block.basefee
);
}

/**
* @dev Verify that an external signer signed the paymaster data of a user operation.
* The paymaster data is expected to be the paymaster address, request data and a signature over the entire request parameters.
* paymasterAndData: hexConcat([paymasterAddress, priceSource, abi.encode(validUntil, validAfter, feeToken, exchangeRate, priceMarkup), signature])
* @param userOp The UserOperation struct that represents the current user operation.
* userOpHash The hash of the UserOperation struct.
* @param requiredPreFund The required amount of pre-funding for the paymaster.
* @return context A context string returned by the entry point after successful validation.
* @return validationData An integer returned by the entry point after successful validation.
*/
function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 requiredPreFund)
internal
view
override
returns (bytes memory context, uint256 validationData)
{
(requiredPreFund);

(
ExchangeRateSource priceSource,
uint48 validUntil,
uint48 validAfter,
address feeToken,
uint128 exchangeRate,
uint32 priceMarkup,
bytes calldata signature
) = parsePaymasterAndData(userOp.paymasterAndData);

bytes32 _hash = getHash(userOp, priceSource, validUntil, validAfter, feeToken, exchangeRate, priceMarkup)
.toEthSignedMessageHash();

//don't revert on signature failure: return SIG_VALIDATION_FAILED
if (verifyingSigner != _hash.recover(signature)) {
// empty context and sigFailed true
return (context, Helpers._packValidationData(true, validUntil, validAfter));
}

address account = userOp.getSender();

// This model assumes irrespective of priceSource exchangeRate is always sent from outside
// for below checks you would either need maxCost or some exchangeRate

uint256 btpmRequiredPrefund = _getRequiredPrefund(userOp);

uint256 tokenRequiredPreFund = (btpmRequiredPrefund * exchangeRate) / 10 ** 18;
require(priceMarkup <= 2e6, "BTPM: price markup percentage too high");
require(
IERC20(feeToken).balanceOf(account) >= ((tokenRequiredPreFund * priceMarkup) / PRICE_DENOMINATOR),
"BTPM: Insufficient token balance"
);

context = abi.encode(account, feeToken, priceSource, exchangeRate, priceMarkup, userOp.maxFeePerGas, userOp.maxPriorityFeePerGas, userOpHash);

return (context, Helpers._packValidationData(false, validUntil, validAfter));
}

function _withdrawERC20(IERC20 token, address target, uint256 amount) private {
if (target == address(0)) revert CanNotWithdrawToZeroAddress();
SafeTransferLib.safeTransfer(address(token), target, amount);
}

receive() external payable {
emit Received(msg.sender, msg.value);
emit TokensWithdrawn(address(token), target, amount, msg.sender);
}
}
13 changes: 7 additions & 6 deletions contracts/token/oracles/IOracleAggregator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
pragma solidity ^0.8.23;

interface IOracleAggregator {
error MismatchInBaseAndQuoteDecimals();
error InvalidPriceFromRound();
error LatestRoundIncomplete();
error PriceFeedStale();
error OracleAddressCannotBeZero();

struct TokenInfo {
uint8 tokenDecimals;
uint24 priceUpdateThreshold;
address tokenOracle;
address nativeOracle;
bool isDerivedFeed;
}

error MismatchInBaseAndQuoteDecimals();
error InvalidPriceFromRound();
error LatestRoundIncomplete();
error PriceFeedStale();
error OracleAddressCannotBeZero();

function getTokenValueOfOneNativeToken(address _token) external view returns (uint128 exchangeRate);
}
26 changes: 15 additions & 11 deletions contracts/token/oracles/OracleAggregator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./IOracleAggregator.sol";
import "./FeedInterface.sol";

Expand All @@ -14,10 +15,10 @@ abstract contract OracleAggregator is Ownable, IOracleAggregator {

function setTokenOracle(
address token,
uint8 tokenDecimals,
address tokenOracle,
address nativeOracle,
bool isDerivedFeed
bool isDerivedFeed,
uint24 priceUpdateThreshold
) external onlyOwner {
if (tokenOracle == address(0)) revert OracleAddressCannotBeZero();
if (nativeOracle == address(0)) revert OracleAddressCannotBeZero();
Expand All @@ -27,8 +28,9 @@ abstract contract OracleAggregator is Ownable, IOracleAggregator {
if (decimals1 != decimals2) revert MismatchInBaseAndQuoteDecimals();
tokensInfo[token].tokenOracle = tokenOracle;
tokensInfo[token].nativeOracle = nativeOracle;
tokensInfo[token].tokenDecimals = tokenDecimals;
tokensInfo[token].tokenDecimals = ERC20(token).decimals();
tokensInfo[token].isDerivedFeed = isDerivedFeed;
tokensInfo[token].priceUpdateThreshold = priceUpdateThreshold;
Comment on lines +31 to +33
Copy link
Contributor

Choose a reason for hiding this comment

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

Current function setTokenOracle updates TokenInfo in tokensInfo map separately.

I'll suggest to pass TokenInfo directly and update map once. Saves SSTORE operation

tokensInfo[token] = TokenInfo({
    tokenDecimals: ERC20(token).decimals(),
    priceUpdateThreshold,
    tokenOracle,
    nativeOracle,
    isDerivedFeed
});

This way we use less gas

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

can we do this as part of another PR? first I will check gas consumptions on forge. this also requires major changes again in scripts

}

/**
Expand All @@ -50,18 +52,19 @@ abstract contract OracleAggregator is Ownable, IOracleAggregator {
view
returns (uint256 tokenPrice, uint8 tokenOracleDecimals, uint8 tokenDecimals, bool isError)
{
TokenInfo storage tokenInfo = tokensInfo[token];
TokenInfo memory tokenInfo = tokensInfo[token];
tokenDecimals = tokenInfo.tokenDecimals;
uint24 priceUpdateThreshold = tokenInfo.priceUpdateThreshold;

if (tokenInfo.isDerivedFeed) {
(uint256 price1, bool isError1) = fetchPrice(FeedInterface(tokenInfo.nativeOracle));
(uint256 price2, bool isError2) = fetchPrice(FeedInterface(tokenInfo.tokenOracle));
(uint256 price1, bool isError1) = fetchPrice(FeedInterface(tokenInfo.nativeOracle), priceUpdateThreshold);
(uint256 price2, bool isError2) = fetchPrice(FeedInterface(tokenInfo.tokenOracle), priceUpdateThreshold);
isError = isError1 || isError2;
if (isError) return (0, 0, 0, isError);
tokenPrice = (price2 * (10 ** 18)) / price1;
tokenOracleDecimals = 18;
} else {
(tokenPrice, isError) = fetchPrice(FeedInterface(tokenInfo.tokenOracle));
(tokenPrice, isError) = fetchPrice(FeedInterface(tokenInfo.tokenOracle), priceUpdateThreshold);
tokenOracleDecimals = FeedInterface(tokenInfo.tokenOracle).decimals();
}
}
Expand All @@ -70,17 +73,18 @@ abstract contract OracleAggregator is Ownable, IOracleAggregator {
* @dev This function is used to get the latest price from the tokenOracle or nativeOracle.
* @notice Fetches the latest price from the given Oracle.
* @param _oracle The Oracle contract to fetch the price from.
* @param _priceUpdateThreshold The time after which the price is considered stale.
* @return price The latest price fetched from the Oracle.
*/
function fetchPrice(FeedInterface _oracle) internal view returns (uint256 price, bool isError) {
function fetchPrice(FeedInterface _oracle, uint24 _priceUpdateThreshold) internal view returns (uint256 price, bool isError) {
try _oracle.latestRoundData() returns (
uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound
) {
// validateRound
if (answer <= 0) return (0, true);
// 2 days old price is considered stale since the price is updated every 24 hours
if (updatedAt < block.timestamp - 60 * 60 * 24 * 2) return (0, true);
if (answeredInRound < roundId) return (0, true);
// price older than set _priceUpdateThreshold is considered stale
// _priceUpdateThreshold for oracle feed is usually heartbeat interval + block time + buffer
if (updatedAt < block.timestamp - _priceUpdateThreshold) return (0, true);
price = uint256(answer);
return (price, false);
} catch Error(string memory reason) {
Expand Down
Loading
Loading