diff --git a/src/contracts/Staking.sol b/src/contracts/Staking.sol index 33d12c6..b79102d 100644 --- a/src/contracts/Staking.sol +++ b/src/contracts/Staking.sol @@ -14,9 +14,18 @@ import "../interfaces/ITokeReward.sol"; import "../interfaces/ILiquidityReserve.sol"; import "../interfaces/ICurvePool.sol"; import "../interfaces/ICowSettlement.sol"; +import "../structs/CowSwapData.sol"; contract Staking is OwnableUpgradeable, StakingStorage { using SafeERC20Upgradeable for IERC20Upgradeable; + uint256 internal constant COW_UID_LENGTH = 56; + bytes32 internal COW_DOMAIN_SEPARATOR; + bytes32 private constant COW_TYPE_HASH = + hex"d5a25ba2e97094ad7d83dc28a6572da797d6b3e7fc6663bd93efb789fc17e489"; + bytes32 public constant COW_KIND_SELL = + hex"f3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775"; + bytes32 public constant COW_BALANCE_ERC20 = + hex"5a28e9363bb942b639270062aa6bb295f434bcdfc42c97267bf003f272060dc9"; event LogSetEpochDuration(uint256 indexed blockNumber, uint256 duration); event LogSetWarmUpPeriod(uint256 indexed blockNumber, uint256 period); @@ -71,6 +80,7 @@ contract Staking is OwnableUpgradeable, StakingStorage { FEE_ADDRESS = _feeAddress; CURVE_POOL = _curvePool; COW_SETTLEMENT = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; + COW_DOMAIN_SEPARATOR = ICowSettlement(COW_SETTLEMENT).domainSeparator(); COW_RELAYER = 0xC92E8bdf79f0507f65a392b0ab4667716BFE0110; timeLeftToRequestWithdrawal = 12 hours; @@ -762,11 +772,131 @@ contract Staking is OwnableUpgradeable, StakingStorage { } } + /** + * @dev Return the EIP-712 signing hash for the specified order. + * @param order The order to compute the EIP-712 signing hash for. + * @param separator The EIP-712 domain separator to use. + * @return orderDigest The 32 byte EIP-712 struct hash. + **/ + function getHash(CowSwapData memory order, bytes32 separator) + internal + pure + returns (bytes32 orderDigest) + { + bytes32 structHash; + + // NOTE: Compute the EIP-712 order struct hash in place. As suggested + // in the EIP proposal, noting that the order struct has 10 fields, and + // including the type hash `(12 + 1) * 32 = 416` bytes to hash. + // + // solhint-disable-next-line no-inline-assembly + assembly { + let dataStart := sub(order, 32) + let temp := mload(dataStart) + mstore(dataStart, COW_TYPE_HASH) + structHash := keccak256(dataStart, 416) + mstore(dataStart, temp) + } + + // NOTE: Now that we have the struct hash, compute the EIP-712 signing + // hash using scratch memory past the free memory pointer. The signing + // hash is computed from `"\x19\x01" || domainSeparator || structHash`. + // + // + // solhint-disable-next-line no-inline-assembly + assembly { + let freeMemoryPointer := mload(0x40) + mstore(freeMemoryPointer, "\x19\x01") + mstore(add(freeMemoryPointer, 2), separator) + mstore(add(freeMemoryPointer, 34), structHash) + orderDigest := keccak256(freeMemoryPointer, 66) + } + } + + /** + * @dev Packs order UID parameters into the specified memory location. The + * result is equivalent to `abi.encodePacked(...)` with the difference that + * it allows re-using the memory for packing the order UID. + * This function reverts if the order UID buffer is not the correct size. + * + * @param orderUid The buffer pack the order UID parameters into. + * @param orderDigest The EIP-712 struct digest derived from the order + * parameters. + * @param owner The address of the user who owns this order. + * @param validTo The epoch time at which the order will stop being valid. + **/ + function packOrderUidParams( + bytes memory orderUid, + bytes32 orderDigest, + address owner, + uint32 validTo + ) internal pure { + require(orderUid.length == COW_UID_LENGTH, "GPv2: uid buffer overflow"); + + // NOTE: Write the order UID to the allocated memory buffer. The order + // parameters are written to memory in **reverse order** as memory + // operations write 32-bytes at a time and we want to use a packed + // encoding. This means, for example, that after writing the value of + // `owner` to bytes `20:52`, writing the `orderDigest` to bytes `0:32` + // will **overwrite** bytes `20:32`. This is desirable as addresses are + // only 20 bytes and `20:32` should be `0`s: + // + // | 1111111111222222222233333333334444444444555555 + // byte | 01234567890123456789012345678901234567890123456789012345 + // -------+--------------------------------------------------------- + // field | [.........orderDigest..........][......owner.......][vT] + // -------+--------------------------------------------------------- + // mstore | [000000000000000000000000000.vT] + // | [00000000000.......owner.......] + // | [.........orderDigest..........] + // + // Additionally, since Solidity `bytes memory` are length prefixed, + // 32 needs to be added to all the offsets. + // + // solhint-disable-next-line no-inline-assembly + assembly { + mstore(add(orderUid, 56), validTo) + mstore(add(orderUid, 52), owner) + mstore(add(orderUid, 32), orderDigest) + } + } + + /** + * @dev reconstruct the orderUid for a swap through cowswap + * @param orderData The order to compute the EIP-712 signing hash for. + **/ + function getOrderID(CowSwapData calldata orderData) + public + view + returns (bytes memory) + { + // Allocated + bytes memory orderUid = new bytes(COW_UID_LENGTH); + + // Get the hash + bytes32 digest = getHash(orderData, COW_DOMAIN_SEPARATOR); + packOrderUidParams(orderUid, digest, address(this), orderData.validTo); + + return orderUid; + } + /** * @notice trades rewards generated from claimFromTokemak for staking token * @dev this is function is called from claimFromTokemak if the autoRebase bool is set to true */ - function preSign(bytes calldata orderUid) external onlyOwner { + function preSign(CowSwapData calldata data, bytes calldata orderUid) + external + onlyOwner + { + bytes memory derivedOrderID = getOrderID(data); + require( + keccak256(derivedOrderID) == keccak256(orderUid), + "OrderID mismatch" + ); + + require(data.validTo > block.timestamp, "Expired transaction"); + require(data.receiver == address(this), "Invalid receiver"); + ICowSettlement(COW_SETTLEMENT).setPreSignature(orderUid, true); } } diff --git a/src/interfaces/ICowSettlement.sol b/src/interfaces/ICowSettlement.sol index d18c605..c85509d 100644 --- a/src/interfaces/ICowSettlement.sol +++ b/src/interfaces/ICowSettlement.sol @@ -3,4 +3,6 @@ pragma solidity 0.8.9; interface ICowSettlement { function setPreSignature(bytes calldata orderUid, bool signed) external; + + function domainSeparator() external returns (bytes32); } diff --git a/src/structs/CowSwapData.sol b/src/structs/CowSwapData.sol new file mode 100644 index 0000000..a2b4ed5 --- /dev/null +++ b/src/structs/CowSwapData.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.9; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +struct CowSwapData { + IERC20 sellToken; + IERC20 buyToken; + address receiver; + uint256 sellAmount; + uint256 buyAmount; + uint32 validTo; + bytes32 appData; + uint256 feeAmount; + bytes32 kind; + bool partiallyFillable; + bytes32 sellTokenBalance; + bytes32 buyTokenBalance; +} diff --git a/test/stakingTest.ts b/test/stakingTest.ts index cd390cf..a225063 100644 --- a/test/stakingTest.ts +++ b/test/stakingTest.ts @@ -2116,9 +2116,9 @@ describe("Staking", function () { describe("tokemak", function () { // skipping due to order failing sometimes when called in succession // tests cow swap order & presign - it.skip("Trades TOKE to stakingToken on CoW Protocol", async () => { + it("Trades TOKE to stakingToken on CoW Protocol", async () => { const cowSettlement = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41"; - const transferAmount = "76000000000000000000000"; + const transferAmount = "76000000000000000000"; await network.provider.request({ method: "hardhat_impersonateAccount", @@ -2134,43 +2134,62 @@ describe("Staking", function () { BigNumber.from(transferAmount) ); - const response = await axios.post( - "https://api.cow.fi/mainnet/api/v1/quote", - { - sellToken: constants.TOKE_TOKEN, // constants.TOKE_TOKEN, // address of token sold - buyToken: constants.STAKING_TOKEN, // constants.STAKING_TOKEN, // address of token bought - receiver: staking.address, // address that receives proceedings of trade, if zero then user who signed - validTo: 2281625458, // timestamp until order is valid - appData: - "0x0000000000000000000000000000000000000000000000000000000000000000", // extra information - partiallyFillable: false, - sellTokenBalance: "erc20", - buyTokenBalance: "erc20", - from: staking.address, - kind: "sell", // sell or buy - sellAmountBeforeFee: transferAmount, // amount before fee - } - ); - expect(response.status).eq(200); - - const orderUid = await axios.post( - "https://api.cow.fi/mainnet/api/v1/orders", - { - ...response.data.quote, - signingScheme: "presign", - signature: staking.address, - from: staking.address, - } - ); - - const cowSettlementContract = new ethers.Contract( - cowSettlement, - cowSettlementAbi, - accounts[0] - ); - await expect(staking.preSign(orderUid.data)) - .to.emit(cowSettlementContract, "PreSignature") - .withArgs(staking.address, orderUid.data, true); + try { + const response = await axios.post( + "https://api.cow.fi/mainnet/api/v1/quote", + { + sellToken: constants.TOKE_TOKEN, // address of token sold + buyToken: constants.STAKING_TOKEN, // address of token bought + receiver: staking.address, // address that receives proceedings of trade, if zero then user who signed + validTo: Math.round(Date.now() / 1000 + 10000), // timestamp until order is valid + appData: + "0x0000000000000000000000000000000000000000000000000000000000000000", // extra information + partiallyFillable: false, + sellTokenBalance: "erc20", + buyTokenBalance: "erc20", + from: staking.address, + kind: "sell", // sell or buy + sellAmountBeforeFee: transferAmount, // amount before fee + } + ); + + expect(response.status).eq(200); + + const orderUid = await axios.post( + "https://api.cow.fi/mainnet/api/v1/orders", + { + ...response.data.quote, + signingScheme: "presign", + signature: staking.address, + from: staking.address, + } + ); + + const cowSettlementContract = new ethers.Contract( + cowSettlement, + cowSettlementAbi, + accounts[0] + ); + + const kind = await staking.COW_KIND_SELL(); + const balanceERC20 = await staking.COW_BALANCE_ERC20(); + await expect( + staking.preSign( + { + ...response.data.quote, + kind, + sellTokenBalance: balanceERC20, + buyTokenBalance: balanceERC20, + }, + orderUid.data + ) + ) + .to.emit(cowSettlementContract, "PreSignature") + .withArgs(staking.address, orderUid.data, true); + } catch (e) { + console.error("Error:", e); + throw new Error("Failed"); + } }); it("Fails when incorrectly claims/transfer TOKE", async () => { const { staker1 } = await getNamedAccounts();