Skip to content

Commit

Permalink
Add PayForGas event and reverty flag; add price check; add unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
kevincheng96 committed Apr 30, 2024
1 parent 778b6f5 commit 7354cac
Show file tree
Hide file tree
Showing 4 changed files with 503 additions and 31 deletions.
13 changes: 9 additions & 4 deletions src/quark-core-scripts/src/Paycall.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@ contract Paycall {

event PayForGas(address indexed payer, address indexed payee, address indexed paymentToken, uint256 amount);

error BadPrice();
error InvalidCallContext();
error TransactionTooExpensive();

/// @notice This contract's address
address internal immutable scriptAddress;

/// @notice Native token (e.g. ETH) based price feed address (e.g. ETH/USD, ETH/BTC)
address public immutable nativeTokenBasedPriceFeedAddress;

Expand All @@ -30,6 +28,9 @@ contract Paycall {
/// @notice Flag for indicating if reverts from the call should be propagated or swallowed
bool public immutable propagateReverts;

/// @notice This contract's address
address internal immutable scriptAddress;

/// @notice Constant buffer for gas overhead
/// This is a constant to account for the gas used by the Paycall contract itself that's not tracked by gasleft()
uint256 internal constant GAS_OVERHEAD = 75000;
Expand All @@ -52,7 +53,7 @@ contract Paycall {
// Note: Assumes the native token has 18 decimals
divisorScale = 10
** uint256(
18 + AggregatorV3Interface(ethBasedPriceFeedAddress).decimals()
18 + AggregatorV3Interface(nativeTokenBasedPriceFeedAddress).decimals()
- IERC20Metadata(paymentTokenAddress).decimals()
);
}
Expand Down Expand Up @@ -82,6 +83,10 @@ contract Paycall {
}

(, int256 price,,,) = AggregatorV3Interface(nativeTokenBasedPriceFeedAddress).latestRoundData();
if (price <= 0) {
revert BadPrice();
}

uint256 gasUsed = gasInitial - gasleft() + GAS_OVERHEAD;
uint256 paymentAmount = gasUsed * tx.gasprice * uint256(price) / divisorScale;
if (paymentAmount > maxPaymentCost) {
Expand Down
65 changes: 42 additions & 23 deletions src/quark-core-scripts/src/Quotecall.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,26 @@ import "openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
contract Quotecall {
using SafeERC20 for IERC20;

error InvalidCallContext();
error TransactionOutsideQuote();
event PayForGas(address indexed payer, address indexed payee, address indexed paymentToken, uint256 amount);

/// @notice This contract's address
address internal immutable scriptAddress;
error BadPrice();
error InvalidCallContext();
error QuoteToleranceExceeded();

/// @notice Native token (e.g. ETH) based price feed address (e.g. ETH/USD, ETH/BTC)
address public immutable nativeTokenBasedPriceFeedAddress;

/// @notice Payment token address
address public immutable paymentTokenAddress;

/// @notice The max delta in basis points
uint256 public immutable maxDeltaBps;
/// @notice The max delta precentage allowed between the quoted cost and actual cost of the call
uint256 public immutable maxDeltaPercentage;

/// @notice Flag for indicating if reverts from the call should be propagated or swallowed
bool public immutable propagateReverts;

/// @notice This contract's address
address internal immutable scriptAddress;

/// @notice Constant buffer for gas overhead
/// This is a constant to accounted for the gas used by the Quotecall contract itself that's not tracked by gasleft()
Expand All @@ -35,22 +41,32 @@ contract Quotecall {
/// @notice Difference in scale between the native token + price feed and the payment token, used to scale the payment token
uint256 internal immutable divisorScale;

/// @dev The scale for percentages, used for `maxDeltaPercentage` (e.g. 1e18 = 100%)
uint256 internal constant PERCENTAGE_SCALE = 1e18;

/**
* @notice Constructor
* @param nativeTokenBasedPriceFeedAddress_ Native token based price feed address that follows Chainlink's AggregatorV3Interface correlated to the payment token
* @param paymentTokenAddress_ Payment token address
* @param maxDeltaBps_ Maximal allowed delta in basis points (100 bps = 1%)
* @param maxDeltaPercentage_ Maximum allowed delta percentage between the quoted cost and actual cost of the call (1e18 = 100%)
* @param propagateReverts_ Flag for indicating if reverts from the call should be propagated or swallowed
*/
constructor(address nativeTokenBasedPriceFeedAddress_, address paymentTokenAddress_, uint256 maxDeltaBps_) {
constructor(
address nativeTokenBasedPriceFeedAddress_,
address paymentTokenAddress_,
uint256 maxDeltaPercentage_,
bool propagateReverts_
) {
nativeTokenBasedPriceFeedAddress = nativeTokenBasedPriceFeedAddress_;
paymentTokenAddress = paymentTokenAddress_;
maxDeltaBps = maxDeltaBps_;
maxDeltaPercentage = maxDeltaPercentage_;
propagateReverts = propagateReverts_;
scriptAddress = address(this);

// Note: Assumes the native token has 18 decimals
divisorScale = 10
** uint256(
18 + AggregatorV3Interface(ethBasedPriceFeedAddress).decimals()
18 + AggregatorV3Interface(nativeTokenBasedPriceFeedAddress).decimals()
- IERC20Metadata(paymentTokenAddress).decimals()
);
}
Expand All @@ -59,36 +75,39 @@ contract Quotecall {
* @notice Execute delegatecall on a contract and pay tx.origin for gas
* @param callContract Contract to call
* @param callData Encoded calldata for call
* @param quote The quoted network fee for this transaction
* @param quotedAmount The quoted network fee for this transaction, in units of the payment token
* @return Return data from call
*/
function run(address callContract, bytes calldata callData, uint256 quote)
external
returns (bytes memory)
{
function run(address callContract, bytes calldata callData, uint256 quotedAmount) external returns (bytes memory) {
uint256 gasInitial = gasleft();
// Ensures that this script cannot be called directly and self-destructed
if (address(this) == scriptAddress) {
revert InvalidCallContext();
}

IERC20(paymentTokenAddress).safeTransfer(tx.origin, quote);
IERC20(paymentTokenAddress).safeTransfer(tx.origin, quotedAmount);
emit PayForGas(address(this), tx.origin, paymentTokenAddress, quotedAmount);

(bool success, bytes memory returnData) = callContract.delegatecall(callData);
if (!success) {
if (!success && propagateReverts) {
assembly {
revert(add(returnData, 32), mload(returnData))
}
}

(, int256 price,,,) = AggregatorV3Interface(ethBasedPriceFeedAddress).latestRoundData();
(, int256 price,,,) = AggregatorV3Interface(nativeTokenBasedPriceFeedAddress).latestRoundData();
if (price <= 0) {
revert BadPrice();
}

uint256 gasUsed = gasInitial - gasleft() + GAS_OVERHEAD;
uint256 expectedAmount = gasUsed * tx.gasprice * uint256(price) / divisorScale;
uint256 actualDelta = expectedAmount > quote ? expectedAmount - quote : quote - expectedAmount;
uint256 actualDeltaBps = actualDelta * 10000 / quote;
uint256 actualAmount = gasUsed * tx.gasprice * uint256(price) / divisorScale;
uint256 actualDelta = actualAmount > quotedAmount ? actualAmount - quotedAmount : quotedAmount - actualAmount;
uint256 actualDeltaPercentage = actualDelta * PERCENTAGE_SCALE / quotedAmount;
emit PayForGas(address(this), tx.origin, paymentTokenAddress, actualAmount);

if (actualDeltaBps > maxDelta) {
revert TransactionOutsideQuote();
if (actualDeltaPercentage > maxDeltaPercentage) {
revert QuoteToleranceExceeded();
}

return returnData;
Expand Down
12 changes: 8 additions & 4 deletions test/quark-core-scripts/Paycall.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ contract PaycallTest is Test {
bytes multicall = new YulHelper().getCode("Multicall.sol/Multicall.json");
bytes ethcall = new YulHelper().getCode("Ethcall.sol/Ethcall.json");
bytes reverts = new YulHelper().getCode("Reverts.sol/Reverts.json");
// Paycall has its contructor with 2 parameters

// Paycall has its contructor with 3 parameters
bytes paycall;
bytes paycallUSDT;
bytes paycallWBTC;
Expand Down Expand Up @@ -108,10 +109,13 @@ contract PaycallTest is Test {
/* ===== call context-based tests ===== */

function testInitializeProperlyFromConstructor() public {
address storedPriceFeedAddress = Paycall(paycallAddress).ethBasedPriceFeedAddress();
address stpredPaymentTokenAddress = Paycall(paycallAddress).paymentTokenAddress();
address storedPriceFeedAddress = Paycall(paycallAddress).nativeTokenBasedPriceFeedAddress();
address storedPaymentTokenAddress = Paycall(paycallAddress).paymentTokenAddress();
bool storedPropagateReverts = Paycall(paycallAddress).propagateReverts();

assertEq(storedPriceFeedAddress, ETH_USD_PRICE_FEED);
assertEq(stpredPaymentTokenAddress, USDC);
assertEq(storedPaymentTokenAddress, USDC);
assertEq(storedPropagateReverts, true);
}

function testRevertsForInvalidCallContext() public {
Expand Down
Loading

0 comments on commit 7354cac

Please sign in to comment.