diff --git a/test/e2e/SmartOrder.t.sol b/test/e2e/SmartOrder.t.sol index af75b61d..c3dc2dbb 100644 --- a/test/e2e/SmartOrder.t.sol +++ b/test/e2e/SmartOrder.t.sol @@ -3,16 +3,116 @@ pragma solidity ^0.8; import {Vm} from "forge-std/Vm.sol"; +import {GPv2Settlement} from "src/contracts/GPv2Settlement.sol"; +import {EIP1271Verifier, GPv2EIP1271} from "src/contracts/interfaces/GPv2EIP1271.sol"; import {IERC20} from "src/contracts/interfaces/IERC20.sol"; import {GPv2Order} from "src/contracts/libraries/GPv2Order.sol"; +import {GPv2SafeERC20} from "src/contracts/libraries/GPv2SafeERC20.sol"; +import {SafeMath} from "src/contracts/libraries/SafeMath.sol"; import {GPv2Signing} from "src/contracts/mixins/GPv2Signing.sol"; import {Sign} from "../libraries/Sign.sol"; import {SettlementEncoder} from "../libraries/encoders/SettlementEncoder.sol"; import {Registry, TokenRegistry} from "../libraries/encoders/TokenRegistry.sol"; -import {SmartSellOrder} from "../src/SmartSellOrder.sol"; import {Helper, IERC20Mintable} from "./Helper.sol"; +/// @title Proof of Concept Smart Order +/// @author Gnosis Developers +contract SmartSellOrder is EIP1271Verifier { + using GPv2Order for GPv2Order.Data; + using GPv2SafeERC20 for IERC20; + using SafeMath for uint256; + + bytes32 public constant APPDATA = keccak256("SmartSellOrder"); + + address public immutable owner; + bytes32 public immutable domainSeparator; + IERC20 public immutable sellToken; + IERC20 public immutable buyToken; + uint256 public immutable totalSellAmount; + uint256 public immutable totalFeeAmount; + uint32 public immutable validTo; + + constructor( + GPv2Settlement settlement, + IERC20 sellToken_, + IERC20 buyToken_, + uint32 validTo_, + uint256 totalSellAmount_, + uint256 totalFeeAmount_ + ) { + owner = msg.sender; + domainSeparator = settlement.domainSeparator(); + sellToken = sellToken_; + buyToken = buyToken_; + validTo = validTo_; + totalSellAmount = totalSellAmount_; + totalFeeAmount = totalFeeAmount_; + + sellToken_.approve(address(settlement.vaultRelayer()), type(uint256).max); + } + + modifier onlyOwner() { + require(msg.sender == owner, "not owner"); + _; + } + + function withdraw(uint256 amount) external onlyOwner { + sellToken.safeTransfer(owner, amount); + } + + function close() external onlyOwner { + uint256 balance = sellToken.balanceOf(address(this)); + if (balance != 0) { + sellToken.safeTransfer(owner, balance); + } + } + + function isValidSignature(bytes32 hash, bytes memory signature) + external + view + override + returns (bytes4 magicValue) + { + uint256 sellAmount = abi.decode(signature, (uint256)); + GPv2Order.Data memory order = orderForSellAmount(sellAmount); + + if (order.hash(domainSeparator) == hash) { + magicValue = GPv2EIP1271.MAGICVALUE; + } + } + + function orderForSellAmount(uint256 sellAmount) public view returns (GPv2Order.Data memory order) { + order.sellToken = sellToken; + order.buyToken = buyToken; + order.receiver = owner; + order.sellAmount = sellAmount; + order.buyAmount = buyAmountForSellAmount(sellAmount); + order.validTo = validTo; + order.appData = APPDATA; + order.feeAmount = totalFeeAmount.mul(sellAmount).div(totalSellAmount); + order.kind = GPv2Order.KIND_SELL; + // NOTE: We counter-intuitively set `partiallyFillable` to `false`, even + // if the smart order as a whole acts like a partially fillable order. + // This is done since, once a settlement commits to a specific sell + // amount, then it is expected to use it completely and not partially. + order.partiallyFillable = false; + order.sellTokenBalance = GPv2Order.BALANCE_ERC20; + order.buyTokenBalance = GPv2Order.BALANCE_ERC20; + } + + function buyAmountForSellAmount(uint256 sellAmount) private view returns (uint256 buyAmount) { + uint256 feeAdjustedBalance = + sellToken.balanceOf(address(this)).mul(totalSellAmount).div(totalSellAmount.add(totalFeeAmount)); + uint256 soldAmount = totalSellAmount > feeAdjustedBalance ? totalSellAmount - feeAdjustedBalance : 0; + + // NOTE: This is currently a silly price strategy where the xrate + // increases linearly from 1:1 to 1:2 as the smart order gets filled. + // This can be extended to more complex "price curves". + buyAmount = sellAmount.mul(totalSellAmount.add(sellAmount).add(soldAmount)).div(totalSellAmount); + } +} + using SettlementEncoder for SettlementEncoder.State; using TokenRegistry for TokenRegistry.State; using TokenRegistry for Registry; diff --git a/test/src/SmartSellOrder.sol b/test/src/SmartSellOrder.sol deleted file mode 100644 index 23336f53..00000000 --- a/test/src/SmartSellOrder.sol +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -// solhint-disable-next-line compiler-version -pragma solidity >=0.7.6 <0.9.0; -pragma abicoder v2; - -import "src/contracts/GPv2Settlement.sol"; -import "src/contracts/interfaces/GPv2EIP1271.sol"; -import "src/contracts/interfaces/IERC20.sol"; -import "src/contracts/libraries/GPv2Order.sol"; -import "src/contracts/libraries/GPv2SafeERC20.sol"; -import "src/contracts/libraries/SafeMath.sol"; - -/// @title Proof of Concept Smart Order -/// @author Gnosis Developers -contract SmartSellOrder is EIP1271Verifier { - using GPv2Order for GPv2Order.Data; - using GPv2SafeERC20 for IERC20; - using SafeMath for uint256; - - bytes32 public constant APPDATA = keccak256("SmartSellOrder"); - - address public immutable owner; - bytes32 public immutable domainSeparator; - IERC20 public immutable sellToken; - IERC20 public immutable buyToken; - uint256 public immutable totalSellAmount; - uint256 public immutable totalFeeAmount; - uint32 public immutable validTo; - - constructor( - GPv2Settlement settlement, - IERC20 sellToken_, - IERC20 buyToken_, - uint32 validTo_, - uint256 totalSellAmount_, - uint256 totalFeeAmount_ - ) { - owner = msg.sender; - domainSeparator = settlement.domainSeparator(); - sellToken = sellToken_; - buyToken = buyToken_; - validTo = validTo_; - totalSellAmount = totalSellAmount_; - totalFeeAmount = totalFeeAmount_; - - sellToken_.approve(address(settlement.vaultRelayer()), type(uint256).max); - } - - modifier onlyOwner() { - require(msg.sender == owner, "not owner"); - _; - } - - function withdraw(uint256 amount) external onlyOwner { - sellToken.safeTransfer(owner, amount); - } - - function close() external onlyOwner { - uint256 balance = sellToken.balanceOf(address(this)); - if (balance != 0) { - sellToken.safeTransfer(owner, balance); - } - } - - function isValidSignature(bytes32 hash, bytes memory signature) - external - view - override - returns (bytes4 magicValue) - { - uint256 sellAmount = abi.decode(signature, (uint256)); - GPv2Order.Data memory order = orderForSellAmount(sellAmount); - - if (order.hash(domainSeparator) == hash) { - magicValue = GPv2EIP1271.MAGICVALUE; - } - } - - function orderForSellAmount(uint256 sellAmount) public view returns (GPv2Order.Data memory order) { - order.sellToken = sellToken; - order.buyToken = buyToken; - order.receiver = owner; - order.sellAmount = sellAmount; - order.buyAmount = buyAmountForSellAmount(sellAmount); - order.validTo = validTo; - order.appData = APPDATA; - order.feeAmount = totalFeeAmount.mul(sellAmount).div(totalSellAmount); - order.kind = GPv2Order.KIND_SELL; - // NOTE: We counter-intuitively set `partiallyFillable` to `false`, even - // if the smart order as a whole acts like a partially fillable order. - // This is done since, once a settlement commits to a specific sell - // amount, then it is expected to use it completely and not partially. - order.partiallyFillable = false; - order.sellTokenBalance = GPv2Order.BALANCE_ERC20; - order.buyTokenBalance = GPv2Order.BALANCE_ERC20; - } - - function buyAmountForSellAmount(uint256 sellAmount) private view returns (uint256 buyAmount) { - uint256 feeAdjustedBalance = - sellToken.balanceOf(address(this)).mul(totalSellAmount).div(totalSellAmount.add(totalFeeAmount)); - uint256 soldAmount = totalSellAmount > feeAdjustedBalance ? totalSellAmount - feeAdjustedBalance : 0; - - // NOTE: This is currently a silly price strategy where the xrate - // increases linearly from 1:1 to 1:2 as the smart order gets filled. - // This can be extended to more complex "price curves". - buyAmount = sellAmount.mul(totalSellAmount.add(sellAmount).add(soldAmount)).div(totalSellAmount); - } -}