-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Gas token solution submission (#1045)
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
Showing
4 changed files
with
223 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
_; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters