Skip to content

Commit

Permalink
Gas token solution submission (#1045)
Browse files Browse the repository at this point in the history
This PR adds a wrapper contract around solution submissions that allows burning an appropriate amount of [gas tokens](https://1inch-exchange.medium.com/1inch-introduces-chi-gastoken-d0bd5bb0f92b) to offset the cost of settling an auction.

It's using the same signature for settlement as batch exchange but wraps the call into a modifier taken from the block article above (slightly adjusted to not use `freeFrom` as this is more expensive than if the wrapping contract actually had gas token balance).

It allows setting a threshold below which gas tokens would not be used (as it might not be worth burning tokens in times of loww gas prices)

In order to allow future withdrawl of fee rewards, it also exposes an "execute" method that can make arbitrary calls by the owner (by default the account deploying the contract). Submit solution itself is called permissionlessly by any account.

### Test Plan

Unit tests, once reviewed I will deploy on mainnet and run some "simulations" on how previous solution submissions would have performed before making the solver point at this contract instead of the real exchange.
  • Loading branch information
fleupold authored Nov 20, 2020
1 parent f713208 commit 793d876
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 2 deletions.
77 changes: 77 additions & 0 deletions contracts/SolutionSubmitter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
pragma solidity ^0.5.0;

import "./BatchExchange.sol";
import "@openzeppelin/contracts/ownership/Ownable.sol";

interface GasToken {
function freeUpTo(uint256 value) external returns (uint256 freed);
}

/** @title SolutionSubmitter - A simple wrapper contract allowing to submit settlements
* to the BatchExchange contract while saving gas via the GST2 or CHI GasToken
*/
contract SolutionSubmitter is Ownable {
BatchExchange public exchange;
GasToken public gasToken;
uint256 public gasPriceThreshold;

constructor(
BatchExchange exchange_,
GasToken gasToken_,
uint256 gasPriceThreshold_
) public {
exchange = exchange_;
setGasToken(gasToken_);
setGasPriceThreshold(gasPriceThreshold_);
}

function setGasToken(GasToken gasToken_) public onlyOwner {
gasToken = gasToken_;
}

function setGasPriceThreshold(uint256 gasPriceThreshold_) public onlyOwner {
gasPriceThreshold = gasPriceThreshold_;
}

/**
* @dev Allow the owner to execute arbitrary code (e.g. transfer out solution fee rewards from BatchExchange)
*/
function execute(address target, bytes calldata data) external onlyOwner returns (bool, bytes memory) {
// solium-disable-next-line security/no-low-level-calls
return target.call(data);
}

/**
* @dev Wrapper around actual solution submission that uses gas tokens for discounts
*/
function submitSolution(
uint32 batchId,
uint256 claimedObjectiveValue,
address[] memory owners,
uint16[] memory orderIds,
uint128[] memory buyVolumes,
uint128[] memory prices,
uint16[] memory tokenIdsForPrice
) public gasDiscounted returns (uint256) {
return exchange.submitSolution(batchId, claimedObjectiveValue, owners, orderIds, buyVolumes, prices, tokenIdsForPrice);
}

/**
* @dev Modifier to invoke original method and freeing up to half the maximum gas refund in gas tokens.
* Logic adjusted from https://1inch-exchange.medium.com/1inch-introduces-chi-gastoken-d0bd5bb0f92b
*/
modifier gasDiscounted {
if (tx.gasprice >= gasPriceThreshold) {
uint256 gasStart = gasleft();
_;
uint256 gasSpent = 21000 + gasStart - gasleft() + (16 * msg.data.length);
// The refund is 24k per token and since we can refund up to half of the total gas spent,
// we should free one gas token for every 48k gas spent. This doesn't account for the cost
// of freeUpTo itself and this slightly underestimating the amout of tokens to burn. This is
// fine as we cannot account for other refunds coming from the solution submission intself.
gasToken.freeUpTo((gasSpent) / 48000);
} else {
_;
}
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-tsdoc": "^0.2.7",
"eth-gas-reporter": "^0.2.19",
"ganache-cli": "^6.12.1",
"ganache-cli": "6.12.0",
"mocha": "^8.2.0",
"prettier": "^2.1.2",
"prettier-plugin-solidity": "^1.0.0-alpha.59",
Expand Down
135 changes: 135 additions & 0 deletions test/SolutionSubmitter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
const ERC20 = artifacts.require("ERC20");
const GasToken = artifacts.require("GasToken");
const MockContract = artifacts.require("MockContract");
const SolutionSubmitter = artifacts.require("SolutionSubmitter");

import truffleAssert from "truffle-assertions";
import {
closeAuction,
makeDeposits,
placeOrders,
setupGenericStableX,
} from "./utilities";
import { solutionSubmissionParams, basicRingTrade } from "./resources/examples";

const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";

contract("SolutionSubmitter", (accounts) => {
const [owner, non_owner] = accounts;

describe("execute", () => {
it("allows arbitrary functions calls on behalf of instance from owner", async function () {
const instance = await SolutionSubmitter.new(
ZERO_ADDRESS,
ZERO_ADDRESS,
0,
{ from: owner },
);

const mock = await MockContract.new();
const mockedToken = await ERC20.at(mock.address);
const transfer = mockedToken.contract.methods
.transfer(owner, 100)
.encodeABI();

await instance.execute(mock.address, transfer, { from: owner });

const invocationCount = await mock.invocationCountForMethod.call(
transfer,
);
assert.equal(1, invocationCount.toNumber());
});

it("reverts functions calls from non-owner", async function () {
const instance = await SolutionSubmitter.new(
ZERO_ADDRESS,
ZERO_ADDRESS,
0,
{ from: owner },
);
const mock = await MockContract.new();
const mockedToken = await ERC20.at(mock.address);
const transfer = mockedToken.contract.methods
.transfer(owner, 100)
.encodeABI();

await truffleAssert.reverts(
instance.execute(mock.address, transfer, { from: non_owner }),
);
});
});

describe("submit solution", () => {
it("frees gas tokens to get ~50% of estimated gas", async () => {
// Use real BatchExchange so we incur some significant gas costs
const exchange = await setupGenericStableX(3);
await makeDeposits(exchange, accounts, basicRingTrade.deposits);

const batchId = (await exchange.getCurrentBatchId()).toNumber();
const orderIds = await placeOrders(
exchange,
accounts,
basicRingTrade.orders,
batchId + 1,
);

await closeAuction(exchange);
const solution = solutionSubmissionParams(
basicRingTrade.solutions[0],
accounts,
orderIds,
);
const { prices, volumes } = solution;

const gasTokenMock = await MockContract.new();
const submitter = await SolutionSubmitter.new(
exchange.address,
gasTokenMock.address,
0,
);

const call = submitter.contract.methods.submitSolution(
batchId,
solution.objectiveValue.toString(),
solution.owners,
solution.touchedorderIds,
volumes.map((v) => v.toString()),
prices.map((p) => p.toString()),
solution.tokenIdsForPrice,
);
const estimate = await call.estimateGas();
await call.send({ from: accounts[0], gas: estimate });

// Each token refunds 24k gas and we can free up to half of the gas used
const expectedTokenFreed = Math.ceil(estimate / 2 / 24000);

const gasToken = await GasToken.at(gasTokenMock.address);
const freeInvocation = gasToken.contract.methods
.freeUpTo(expectedTokenFreed)
.encodeABI();
assert.equal(
1,
(
await gasTokenMock.invocationCountForMethod.call(freeInvocation)
).toNumber(),
);
});

it("respects gasThreshold", async () => {
const exchange = await MockContract.new();
const gasToken = await MockContract.new();
const instance = await SolutionSubmitter.new(
exchange.address,
gasToken.address,
100,
{ from: owner },
);

await instance.submitSolution(1, 1, [], [], [], [], [], {
gasPrice: 50,
});

assert.equal(0, (await gasToken.invocationCount.call()).toNumber());
});
});
});
11 changes: 10 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4636,7 +4636,16 @@ functional-red-black-tree@^1.0.1:
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=

ganache-cli@^6.11.0, ganache-cli@^6.12.1:
ganache-cli@6.12.0:
version "6.12.0"
resolved "https://registry.yarnpkg.com/ganache-cli/-/ganache-cli-6.12.0.tgz#0cfe3ae2287b2bb036c1ec1fa7360c1ff837535b"
integrity sha512-WV354mOSCbVH+qR609ftpz/1zsZPRsHMaQ4jo9ioBQAkguYNVU5arfgIE0+0daU0Vl9WJ/OMhRyl0XRswd/j9A==
dependencies:
ethereumjs-util "6.2.1"
source-map-support "0.5.12"
yargs "13.2.4"

ganache-cli@^6.11.0:
version "6.12.1"
resolved "https://registry.yarnpkg.com/ganache-cli/-/ganache-cli-6.12.1.tgz#148cf6541494ef1691bd68a77e4414981910cb3e"
integrity sha512-zoefZLQpQyEJH9jgtVYgM+ENFLAC9iwys07IDCsju2Ieq9KSTLH89RxSP4bhizXKV/h/+qaWpfyCBGWnBfqgIQ==
Expand Down

0 comments on commit 793d876

Please sign in to comment.