diff --git a/.forge-snapshots/ConditionalTokens_mergePositions.snap b/.forge-snapshots/ConditionalTokens_mergePositions.snap index 4ff9798..fdefb01 100644 --- a/.forge-snapshots/ConditionalTokens_mergePositions.snap +++ b/.forge-snapshots/ConditionalTokens_mergePositions.snap @@ -1 +1 @@ -52410 \ No newline at end of file +67211 \ No newline at end of file diff --git a/.forge-snapshots/ConditionalTokens_splitPosition.snap b/.forge-snapshots/ConditionalTokens_splitPosition.snap index 0326b4e..7dce88d 100644 --- a/.forge-snapshots/ConditionalTokens_splitPosition.snap +++ b/.forge-snapshots/ConditionalTokens_splitPosition.snap @@ -1 +1 @@ -99336 \ No newline at end of file +114137 \ No newline at end of file diff --git a/.forge-snapshots/NegRiskAdapter_convertPositions_32.snap b/.forge-snapshots/NegRiskAdapter_convertPositions_32.snap index 0c3f473..7d40b7e 100644 --- a/.forge-snapshots/NegRiskAdapter_convertPositions_32.snap +++ b/.forge-snapshots/NegRiskAdapter_convertPositions_32.snap @@ -1 +1 @@ -4626380 \ No newline at end of file +4522762 \ No newline at end of file diff --git a/.forge-snapshots/NegRiskAdapter_convertPositions_5.snap b/.forge-snapshots/NegRiskAdapter_convertPositions_5.snap index 319963a..00e69ed 100644 --- a/.forge-snapshots/NegRiskAdapter_convertPositions_5.snap +++ b/.forge-snapshots/NegRiskAdapter_convertPositions_5.snap @@ -1 +1 @@ -687233 \ No newline at end of file +717003 \ No newline at end of file diff --git a/.forge-snapshots/NegRiskAdapter_convertPositions_64.snap b/.forge-snapshots/NegRiskAdapter_convertPositions_64.snap index fa7c0d1..189747c 100644 --- a/.forge-snapshots/NegRiskAdapter_convertPositions_64.snap +++ b/.forge-snapshots/NegRiskAdapter_convertPositions_64.snap @@ -1 +1 @@ -9540525 \ No newline at end of file +9331964 \ No newline at end of file diff --git a/.forge-snapshots/NegRiskAdapter_mergePositions.snap b/.forge-snapshots/NegRiskAdapter_mergePositions.snap index fe57932..c76dea6 100644 --- a/.forge-snapshots/NegRiskAdapter_mergePositions.snap +++ b/.forge-snapshots/NegRiskAdapter_mergePositions.snap @@ -1 +1 @@ -191072 \ No newline at end of file +220694 \ No newline at end of file diff --git a/.forge-snapshots/NegRiskAdapter_prepareMarket.snap b/.forge-snapshots/NegRiskAdapter_prepareMarket.snap index fda2de7..77c4dcd 100644 --- a/.forge-snapshots/NegRiskAdapter_prepareMarket.snap +++ b/.forge-snapshots/NegRiskAdapter_prepareMarket.snap @@ -1 +1 @@ -33098 \ No newline at end of file +33110 \ No newline at end of file diff --git a/.forge-snapshots/NegRiskAdapter_prepareQuestion.snap b/.forge-snapshots/NegRiskAdapter_prepareQuestion.snap index 8cd4eff..8b03e0a 100644 --- a/.forge-snapshots/NegRiskAdapter_prepareQuestion.snap +++ b/.forge-snapshots/NegRiskAdapter_prepareQuestion.snap @@ -1 +1 @@ -37479 \ No newline at end of file +37634 \ No newline at end of file diff --git a/.forge-snapshots/NegRiskAdapter_splitPosition.snap b/.forge-snapshots/NegRiskAdapter_splitPosition.snap index afae6be..87b90bd 100644 --- a/.forge-snapshots/NegRiskAdapter_splitPosition.snap +++ b/.forge-snapshots/NegRiskAdapter_splitPosition.snap @@ -1 +1 @@ -215878 \ No newline at end of file +245589 \ No newline at end of file diff --git a/.forge-snapshots/Vault_transferERC1155.snap b/.forge-snapshots/Vault_transferERC1155.snap index 6f707f1..732edcc 100644 --- a/.forge-snapshots/Vault_transferERC1155.snap +++ b/.forge-snapshots/Vault_transferERC1155.snap @@ -1 +1 @@ -32809 \ No newline at end of file +34921 \ No newline at end of file diff --git a/.forge-snapshots/Vault_transferERC20.snap b/.forge-snapshots/Vault_transferERC20.snap index 4454b22..4d5f78a 100644 --- a/.forge-snapshots/Vault_transferERC20.snap +++ b/.forge-snapshots/Vault_transferERC20.snap @@ -1 +1 @@ -31009 \ No newline at end of file +33121 \ No newline at end of file diff --git a/.forge-snapshots/WrappedCollateral_unwrap.snap b/.forge-snapshots/WrappedCollateral_unwrap.snap index cf6f871..61e3810 100644 --- a/.forge-snapshots/WrappedCollateral_unwrap.snap +++ b/.forge-snapshots/WrappedCollateral_unwrap.snap @@ -1 +1 @@ -28664 \ No newline at end of file +30764 \ No newline at end of file diff --git a/.forge-snapshots/WrappedCollateral_wrap.snap b/.forge-snapshots/WrappedCollateral_wrap.snap index e105afc..7df3907 100644 --- a/.forge-snapshots/WrappedCollateral_wrap.snap +++ b/.forge-snapshots/WrappedCollateral_wrap.snap @@ -1 +1 @@ -75728 \ No newline at end of file +77828 \ No newline at end of file diff --git a/.gitignore b/.gitignore index e38c54b..82356d7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ docs/ # python virtualenv env/ + +node_modules/ diff --git a/.gitmodules b/.gitmodules index b30cd81..daf13ae 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,9 @@ [submodule "lib/forge-gas-snapshot"] path = lib/forge-gas-snapshot url = https://github.com/marktoda/forge-gas-snapshot +[submodule "lib/ctf-exchange"] + path = lib/ctf-exchange + url = https://github.com/Polymarket/ctf-exchange +[submodule "lib/exchange-fee-module"] + path = lib/exchange-fee-module + url = https://github.com/Polymarket/exchange-fee-module diff --git a/.vscode/settings.json b/.vscode/settings.json index 6ced0dc..558c13a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,8 +3,8 @@ "solidity.packageDefaultDependenciesDirectory": "lib", "solidity.compileUsingRemoteVersion": "v0.8.19+commit.7dd6d404", "editor.formatOnSave": true, - "[solidity]": { - "editor.defaultFormatter": "JuanBlanco.solidity" - }, + // "[solidity]": { + // "editor.defaultFormatter": "JuanBlanco.solidity" + // }, "solidity.formatter": "forge" } diff --git a/foundry.toml b/foundry.toml index 30d5add..ac64241 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,11 +1,10 @@ [profile.default] -solc = "0.8.19" src = "src" out = "out" libs = ["lib"] ffi = true -fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"},{ access = "read", path = "./artifacts/"}] +fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"},{ access = "read", path = "./artifacts/"}, { access = "read", path = "./out/"}] [fmt] line_length = 120 diff --git a/lib/ctf-exchange b/lib/ctf-exchange new file mode 160000 index 0000000..2745c30 --- /dev/null +++ b/lib/ctf-exchange @@ -0,0 +1 @@ +Subproject commit 2745c3017400dbc1925711005fe76b018b999155 diff --git a/lib/exchange-fee-module b/lib/exchange-fee-module new file mode 160000 index 0000000..9abb8cd --- /dev/null +++ b/lib/exchange-fee-module @@ -0,0 +1 @@ +Subproject commit 9abb8cd0b9ce57ed1a8c2365dae4c9e7ea02cf20 diff --git a/lib/forge-std b/lib/forge-std index adec12d..267acd3 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit adec12de5e4575a23df075ee4917cb058c2c617d +Subproject commit 267acd30a625086b3f16e1a28cfe0c5097fa46b8 diff --git a/remappings.txt b/remappings.txt index 88db1e1..e3311f9 100644 --- a/remappings.txt +++ b/remappings.txt @@ -2,3 +2,5 @@ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ solmate/=lib/solmate/src/ forge-gas-snapshot/=lib/forge-gas-snapshot/src/ + +openzeppelin-contracts/=lib/ctf-exchange/lib/openzeppelin-contracts/contracts/ \ No newline at end of file diff --git a/src/NegRiskAdapter.sol b/src/NegRiskAdapter.sol index 6d54981..cef36a7 100644 --- a/src/NegRiskAdapter.sol +++ b/src/NegRiskAdapter.sol @@ -4,21 +4,23 @@ pragma solidity 0.8.19; import {ERC1155TokenReceiver} from "lib/solmate/src/tokens/ERC1155.sol"; import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; import {SafeTransferLib} from "lib/solmate/src/utils/SafeTransferLib.sol"; - import {WrappedCollateral} from "src/WrappedCollateral.sol"; import {MarketData, MarketStateManager, IMarketStateManagerEE} from "src/modules/MarketDataManager.sol"; import {CTHelpers} from "src/libraries/CTHelpers.sol"; import {Helpers} from "src/libraries/Helpers.sol"; import {NegRiskIdLib} from "src/libraries/NegRiskIdLib.sol"; import {IConditionalTokens} from "src/interfaces/IConditionalTokens.sol"; +import {Auth} from "src/modules/Auth.sol"; +import {IAuthEE} from "src/modules/interfaces/IAuth.sol"; /// @title INegRiskAdapterEE /// @notice NegRiskAdapter Errors and Events -interface INegRiskAdapterEE is IMarketStateManagerEE { +interface INegRiskAdapterEE is IMarketStateManagerEE, IAuthEE { error InvalidIndexSet(); error LengthMismatch(); error UnexpectedCollateralToken(); error NoConvertiblePositions(); + error NotApprovedForAll(); event MarketPrepared(bytes32 indexed marketId, address indexed oracle, uint256 feeBips, bytes data); event QuestionPrepared(bytes32 indexed marketId, bytes32 indexed questionId, uint256 index, bytes data); @@ -37,7 +39,7 @@ interface INegRiskAdapterEE is IMarketStateManagerEE { /// @notice And the adapter allows for the conversion of a set of no positions, to collateral plus the set of /// complementary yes positions /// @author Mike Shrieve (mike@polymarket.com) -contract NegRiskAdapter is ERC1155TokenReceiver, MarketStateManager, INegRiskAdapterEE { +contract NegRiskAdapter is ERC1155TokenReceiver, MarketStateManager, INegRiskAdapterEE, Auth { using SafeTransferLib for ERC20; /*////////////////////////////////////////////////////////////// @@ -56,7 +58,7 @@ contract NegRiskAdapter is ERC1155TokenReceiver, MarketStateManager, INegRiskAda CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - /// @param _ctf - ConditionalTokens address + /// @param _ctf - ConditionalTokens address /// @param _collateral - collateral address constructor(address _ctf, address _collateral, address _vault) { ctf = IConditionalTokens(_ctf); @@ -118,7 +120,7 @@ contract NegRiskAdapter is ERC1155TokenReceiver, MarketStateManager, INegRiskAda /// @notice Splits collateral to a complete set of conditional tokens for a single question /// @param _conditionId - the conditionId for the question - /// @param _amount - the amount of collateral to split + /// @param _amount - the amount of collateral to split function splitPosition(bytes32 _conditionId, uint256 _amount) public { col.safeTransferFrom(msg.sender, address(this), _amount); wcol.wrap(address(this), _amount); @@ -137,8 +139,8 @@ contract NegRiskAdapter is ERC1155TokenReceiver, MarketStateManager, INegRiskAda /// @notice Merges a complete set of conditional tokens for a single question to collateral /// @notice This function signature is the same as the CTF's mergePositions /// @param _collateralToken - the collateral token, must be the same as the adapter's collateral token - /// @param _conditionId - the conditionId for the question - /// @param _amount - the amount of collateral to merge + /// @param _conditionId - the conditionId for the question + /// @param _amount - the amount of collateral to merge function mergePositions( address _collateralToken, bytes32, @@ -152,7 +154,7 @@ contract NegRiskAdapter is ERC1155TokenReceiver, MarketStateManager, INegRiskAda /// @notice Merges a complete set of conditional tokens for a single question to collateral /// @param _conditionId - the conditionId for the question - /// @param _amount - the amount of collateral to merge + /// @param _amount - the amount of collateral to merge function mergePositions(bytes32 _conditionId, uint256 _amount) public { uint256[] memory positionIds = Helpers.positionIds(address(wcol), _conditionId); @@ -164,13 +166,53 @@ contract NegRiskAdapter is ERC1155TokenReceiver, MarketStateManager, INegRiskAda emit PositionsMerge(msg.sender, _conditionId, _amount); } + /*////////////////////////////////////////////////////////////// + ERC1155 OPERATIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Proxies ERC1155 balanceOf to the CTF + /// @param _owner - the owner of the tokens + /// @param _id - the positionId + /// @return balance - the owner's balance + function balanceOf(address _owner, uint256 _id) external view returns (uint256) { + return ctf.balanceOf(_owner, _id); + } + + /// @notice Proxies ERC1155 balanceOfBatch to the CTF + /// @param _owners - the owners of the tokens + /// @param _ids - the positionIds + /// @return balances - the owners' balances + function balanceOfBatch(address[] memory _owners, uint256[] memory _ids) external view returns (uint256[] memory) { + return ctf.balanceOfBatch(_owners, _ids); + } + + /// @notice Proxies ERC1155 safeTransferFrom to the CTF + /// @notice Can only be called by an admin + /// @notice Requires this contract to be approved for all + /// @notice Requires the sender to be approved for all + /// @param _from - the owner of the tokens + /// @param _to - the recipient of the tokens + /// @param _id - the positionId + /// @param _value - the amount of tokens to transfer + /// @param _data - the data to pass to the recipient + function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) + external + onlyAdmin + { + if (!ctf.isApprovedForAll(_from, msg.sender)) { + revert NotApprovedForAll(); + } + + return ctf.safeTransferFrom(_from, _to, _id, _value, _data); + } + /*////////////////////////////////////////////////////////////// REDEEM POSITION //////////////////////////////////////////////////////////////*/ /// @notice Redeem a set of conditional tokens for collateral /// @param _conditionId - conditionId of the conditional tokens to redeem - /// @param _amounts - amounts of conditional tokens to redeem + /// @param _amounts - amounts of conditional tokens to redeem /// _amounts should always have length 2, with the first element being the amount of yes tokens to redeem and the /// second element being the amount of no tokens to redeem function redeemPositions(bytes32 _conditionId, uint256[] calldata _amounts) public { @@ -373,7 +415,7 @@ contract NegRiskAdapter is ERC1155TokenReceiver, MarketStateManager, INegRiskAda INTERNAL //////////////////////////////////////////////////////////////*/ - /// @dev internal function to avoid stack to deep in convertPositions + /// @dev internal function to avoid stack too deep in convertPositions function _splitPosition(bytes32 _conditionId, uint256 _amount) internal { ctf.splitPosition(address(wcol), bytes32(0), _conditionId, Helpers.partition(), _amount); } diff --git a/src/NegRiskCtfExchange.sol b/src/NegRiskCtfExchange.sol new file mode 100644 index 0000000..77b85a2 --- /dev/null +++ b/src/NegRiskCtfExchange.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {CTFExchange} from "../lib/ctf-exchange/src/exchange/CTFExchange.sol"; +import {IConditionalTokens} from "./interfaces/IConditionalTokens.sol"; + +contract NegRiskCtfExchange is CTFExchange { + constructor(address _collateral, address _ctf, address _negRiskAdapter, address _proxyFactory, address _safeFactory) + CTFExchange(_collateral, _negRiskAdapter, _proxyFactory, _safeFactory) + { + IConditionalTokens(_ctf).setApprovalForAll(_negRiskAdapter, true); + IConditionalTokens(_ctf).setApprovalForAll(address(this), true); + } +} diff --git a/src/NegRiskFeeModule.sol b/src/NegRiskFeeModule.sol new file mode 100644 index 0000000..dbb43c1 --- /dev/null +++ b/src/NegRiskFeeModule.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {FeeModule, IExchange} from "../lib/exchange-fee-module/src/FeeModule.sol"; +import {IConditionalTokens} from "./interfaces/IConditionalTokens.sol"; + +contract NegRiskFeeModule is FeeModule { + constructor(address _negRiskCtfExchange, address _negRiskAdapter, address _ctf) FeeModule(_negRiskCtfExchange) { + IConditionalTokens(_ctf).setApprovalForAll(_negRiskAdapter, true); + IConditionalTokens(_ctf).setApprovalForAll(address(this), true); + } +} diff --git a/src/NegRiskOperator.sol b/src/NegRiskOperator.sol index 003ec18..3fca47b 100644 --- a/src/NegRiskOperator.sol +++ b/src/NegRiskOperator.sol @@ -47,7 +47,7 @@ contract NegRiskOperator is INegRiskOperatorEE, Auth { NegRiskAdapter public immutable nrAdapter; address public oracle; - uint256 public constant DELAY_PERIOD = 12 hours; + uint256 public constant DELAY_PERIOD = 1 hours; mapping(bytes32 _requestId => bytes32) public questionIds; mapping(bytes32 _questionId => bool) public results; @@ -91,8 +91,8 @@ contract NegRiskOperator is INegRiskOperatorEE, Auth { //////////////////////////////////////////////////////////////*/ /// @notice Prepares a market on the NegRiskAdapter - /// @param _feeBips - the market's fee rate out of 10_000 - /// @param _data - the market metadata to be passed to the NegRiskAdapter + /// @param _feeBips - the market's fee rate out of 10_000 + /// @param _data - the market metadata to be passed to the NegRiskAdapter /// @return marketId - the market id function prepareMarket(uint256 _feeBips, bytes calldata _data) external onlyAdmin returns (bytes32) { bytes32 marketId = nrAdapter.prepareMarket(_feeBips, _data); @@ -107,9 +107,10 @@ contract NegRiskOperator is INegRiskOperatorEE, Auth { /// @notice Prepares a question on the NegRiskAdapter /// @notice OnlyAdmin /// @notice Only one question can be prepared per requestId - /// @param _marketId - the id of the market in which to prepare the question - /// @param _data - the question metadata to be passed to the NegRiskAdapter - /// @param _requestId - the question's oracle request id + /// @param _marketId - the id of the market in which to prepare the question + /// @param _data - the question metadata to be passed to the NegRiskAdapter + /// @param _requestId - the question's oracle request id + /// @return questionId - the resulting question id function prepareQuestion(bytes32 _marketId, bytes calldata _data, bytes32 _requestId) external onlyAdmin @@ -136,7 +137,7 @@ contract NegRiskOperator is INegRiskOperatorEE, Auth { /// @notice Only one report can be made per question /// @notice Sets the boolean result and reportedAt timestamp for the question /// @param _requestId - the question's oracle request id - /// @param _payouts - the payouts to be reported, [1,0] if true, [0,1] if false, any other payouts are invalid + /// @param _payouts - the payouts to be reported, [1,0] if true, [0,1] if false, any other payouts are invalid function reportPayouts(bytes32 _requestId, uint256[] calldata _payouts) external onlyOracle { if (_payouts.length != 2) { revert InvalidPayouts(); @@ -211,6 +212,8 @@ contract NegRiskOperator is INegRiskOperatorEE, Auth { /// @notice Resolves a flagged question on the NegRiskAdapter /// @notice OnlyAdmin /// @notice A flagged question can only be resolved if the delay period has passed since the question was flagged + /// @param _questionId - the id of the question to be resolved + /// @param _result - the boolean result of the question function emergencyResolveQuestion(bytes32 _questionId, bool _result) external onlyAdmin { uint256 flaggedAt_ = flaggedAt[_questionId]; diff --git a/src/WrappedCollateral.sol b/src/WrappedCollateral.sol index f07b2c8..8b68cbe 100644 --- a/src/WrappedCollateral.sol +++ b/src/WrappedCollateral.sol @@ -64,8 +64,8 @@ contract WrappedCollateral is IWrappedCollateralEE, ERC20 { /// @notice Wraps the specified amount of tokens /// @notice Can only be called by the owner - /// @param _to The address to send the wrapped tokens to - /// @param _amount The amount of tokens to wrap + /// @param _to - the address to send the wrapped tokens to + /// @param _amount - the amount of tokens to wrap function wrap(address _to, uint256 _amount) external onlyOwner { ERC20(underlying).safeTransferFrom(msg.sender, address(this), _amount); _mint(_to, _amount); @@ -73,22 +73,22 @@ contract WrappedCollateral is IWrappedCollateralEE, ERC20 { /// @notice Burns the specified amount of tokens /// @notice Can only be called by the owner - /// @param _amount The amount of tokens to burn + /// @param _amount - the amount of tokens to burn function burn(uint256 _amount) external onlyOwner { _burn(msg.sender, _amount); } /// @notice Mints the specified amount of tokens /// @notice Can only be called by the owner - /// @param _amount The amount of tokens to mint + /// @param _amount - the amount of tokens to mint function mint(uint256 _amount) external onlyOwner { _mint(msg.sender, _amount); } /// @notice Releases the specified amount of the underlying token /// @notice Can only be called by the owner - /// @param _to The address to send the released tokens to - /// @param _amount The amount of tokens to release + /// @param _to - the address to send the released tokens to + /// @param _amount - the amount of tokens to release function release(address _to, uint256 _amount) external onlyOwner { ERC20(underlying).safeTransfer(_to, _amount); } diff --git a/src/dev/OrderHelper.sol b/src/dev/OrderHelper.sol new file mode 100644 index 0000000..389e5e1 --- /dev/null +++ b/src/dev/OrderHelper.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {Script} from "../../lib/forge-std/src/Script.sol"; +import {stdStorage, StdStorage} from "../../lib/forge-std/src/StdStorage.sol"; +import {Side, SignatureType} from "../../lib/ctf-exchange/src/exchange/libraries/OrderStructs.sol"; + +import {vm} from "./libraries/Vm.sol"; +import {ICTFExchange} from "../interfaces/index.sol"; + +using stdStorage for StdStorage; + +contract OrderHelper is Script { + function _createAndSignOrder( + address _exchange, + uint256 _pk, + uint256 _tokenId, + uint256 _makerAmount, + uint256 _takerAmount, + Side _side + ) internal view returns (ICTFExchange.Order memory) { + address maker = vm.addr(_pk); + ICTFExchange.Order memory order = _createOrder(maker, _tokenId, _makerAmount, _takerAmount, _side); + order.signature = _signMessage(_pk, ICTFExchange(_exchange).hashOrder(order)); + return order; + } + + function _signMessage(uint256 _pk, bytes32 _message) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_pk, _message); + return abi.encodePacked(r, s, v); + } + + function _createOrder(address _maker, uint256 _tokenId, uint256 _makerAmount, uint256 _takerAmount, Side _side) + internal + pure + returns (ICTFExchange.Order memory) + { + ICTFExchange.Order memory order = ICTFExchange.Order({ + salt: 1, + signer: _maker, + maker: _maker, + taker: address(0), + tokenId: _tokenId, + makerAmount: _makerAmount, + takerAmount: _takerAmount, + expiration: 0, + nonce: 0, + feeRateBps: 0, + signatureType: uint8(SignatureType.EOA), + side: uint8(_side), + signature: new bytes(0) + }); + return order; + } +} diff --git a/src/dev/StorageHelper.sol b/src/dev/StorageHelper.sol new file mode 100644 index 0000000..05d67b3 --- /dev/null +++ b/src/dev/StorageHelper.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {Script} from "../../lib/forge-std/src/Script.sol"; +import {stdStorage, StdStorage} from "../../lib/forge-std/src/StdStorage.sol"; +import {vm} from "./libraries/Vm.sol"; + +using stdStorage for StdStorage; + +contract StorageHelper is Script { + function getStorageSlot(address _target, string memory _sig, address _param) public returns (uint256) { + return stdstore.target(_target).sig(_sig).with_key(_param).find(); + } + + function _dealERC1155(address _erc1155, address _account, uint256 _id, uint256 _amount) internal { + stdstore.target(_erc1155).sig("balanceOf(address,uint256)").with_key(_account).with_key(_id).checked_write( + _amount + ); + } + + function _dealERC20(address _erc20, address _account, uint256 _amount) internal { + uint256 storageSlot = getStorageSlot(_erc20, "balanceOf(address)", address(_account)); + vm.store(_erc20, bytes32(storageSlot), bytes32(_amount)); + } + + function _setOperator(address _exchange, address _operator) internal { + uint256 storageSlot = getStorageSlot(_exchange, "operators(address)", _operator); + vm.store(_exchange, bytes32(storageSlot), bytes32(uint256(1))); + } + + function _setAdmin(address _exchange, address _admin) internal { + uint256 storageSlot = getStorageSlot(_exchange, "admins(address)", _admin); + vm.store(_exchange, bytes32(storageSlot), bytes32(uint256(1))); + } +} diff --git a/src/dev/TestHelper.sol b/src/dev/TestHelper.sol index a9a720c..ed7da8b 100644 --- a/src/dev/TestHelper.sol +++ b/src/dev/TestHelper.sol @@ -1,33 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; -import { - Test, - console2 as console, - stdJson, - stdStorage, - StdStorage, - stdError -} from "lib/forge-std/src/Test.sol"; +import {Test, console2 as console, stdJson, stdStorage, StdStorage, stdError} from "lib/forge-std/src/Test.sol"; abstract contract TestHelper is Test { using stdJson for string; - address public immutable alice; - address public immutable brian; - address public immutable carly; - address public immutable devin; + address public alice; + address public brian; + address public carly; + address public devin; constructor() { - alice = _getAndLabelAddress("alice"); - brian = _getAndLabelAddress("brian"); - carly = _getAndLabelAddress("carly"); - devin = _getAndLabelAddress("devin"); - } - - function _getAndLabelAddress(string memory _name) internal returns (address) { - address addr = address(bytes20(keccak256(abi.encode(_name)))); - vm.label(addr, _name); - return addr; + alice = vm.createWallet("alice").addr; + brian = vm.createWallet("brian").addr; + carly = vm.createWallet("carly").addr; + devin = vm.createWallet("devin").addr; } } diff --git a/src/dev/libraries/AddressLib.sol b/src/dev/libraries/AddressLib.sol index 9f208c8..95cf4c9 100644 --- a/src/dev/libraries/AddressLib.sol +++ b/src/dev/libraries/AddressLib.sol @@ -7,7 +7,7 @@ import {stdJson} from "forge-std/Test.sol"; library AddressLib { using stdJson for string; - function getAddress(string memory _name) internal returns (address) { + function getAddress(string memory _name) internal view returns (address) { string memory json = vm.readFile("./addresses.json"); uint256 chainId = block.chainid; diff --git a/src/dev/libraries/DeployLib.sol b/src/dev/libraries/DeployLib.sol index e9f1330..db88358 100644 --- a/src/dev/libraries/DeployLib.sol +++ b/src/dev/libraries/DeployLib.sol @@ -26,4 +26,34 @@ library DeployLib { vm.label(deployment, "UmaCtfAdapter"); return deployment; } + + /// @dev this will not correctly set the initial admin and operator + /// _deployCode will not correctly use the msg.sender + function deployNegRiskCtfExchange( + address _collateral, + address _negRiskAdapter, + address _ctf, + address _proxyFactory, + address _safeFactory + ) public returns (address) { + address deployment = _deployCode( + "out/NegRiskCtfExchange.sol/NegRiskCtfExchange.json", + abi.encode(_collateral, _ctf, _negRiskAdapter, _proxyFactory, _safeFactory) + ); + vm.label(deployment, "NegRiskCtfExchange"); + return deployment; + } + + /// @dev this will not correctly set the initial admin + /// _deployCode will not correctly use the msg.sender + function deployNegRiskFeeModule(address _negRiskCtfExchange, address _negRiskAdapter, address _ctf) + public + returns (address) + { + address deployment = _deployCode( + "out/NegRiskFeeModule.sol/NegRiskFeeModule.json", abi.encode(_negRiskCtfExchange, _negRiskAdapter, _ctf) + ); + vm.label(deployment, "NegRiskFeeModule"); + return deployment; + } } diff --git a/src/interfaces/ICTFExchange.sol b/src/interfaces/ICTFExchange.sol new file mode 100644 index 0000000..719abb3 --- /dev/null +++ b/src/interfaces/ICTFExchange.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +interface ICTFExchange { + event FeeCharged(address indexed receiver, uint256 tokenId, uint256 amount); + event NewAdmin(address indexed newAdminAddress, address indexed admin); + event NewOperator(address indexed newOperatorAddress, address indexed admin); + event OrderCancelled(bytes32 indexed orderHash); + event OrderFilled( + bytes32 indexed orderHash, + address indexed maker, + address indexed taker, + uint256 makerAssetId, + uint256 takerAssetId, + uint256 makerAmountFilled, + uint256 takerAmountFilled, + uint256 fee + ); + event OrdersMatched( + bytes32 indexed takerOrderHash, + address indexed takerOrderMaker, + uint256 makerAssetId, + uint256 takerAssetId, + uint256 makerAmountFilled, + uint256 takerAmountFilled + ); + event ProxyFactoryUpdated(address indexed oldProxyFactory, address indexed newProxyFactory); + event RemovedAdmin(address indexed removedAdmin, address indexed admin); + event RemovedOperator(address indexed removedOperator, address indexed admin); + event SafeFactoryUpdated(address indexed oldSafeFactory, address indexed newSafeFactory); + event TokenRegistered(uint256 indexed token0, uint256 indexed token1, bytes32 indexed conditionId); + event TradingPaused(address indexed pauser); + event TradingUnpaused(address indexed pauser); + + struct Order { + uint256 salt; + address maker; + address signer; + address taker; + uint256 tokenId; + uint256 makerAmount; + uint256 takerAmount; + uint256 expiration; + uint256 nonce; + uint256 feeRateBps; + uint8 side; + uint8 signatureType; + bytes signature; + } + + struct OrderStatus { + bool isFilledOrCancelled; + uint256 remaining; + } + + function addAdmin(address admin_) external; + function addOperator(address operator_) external; + function admins(address) external view returns (uint256); + function cancelOrder(Order memory order) external; + function cancelOrders(Order[] memory orders) external; + function domainSeparator() external view returns (bytes32); + function fillOrder(Order memory order, uint256 fillAmount) external; + function fillOrders(Order[] memory orders, uint256[] memory fillAmounts) external; + function getCollateral() external view returns (address); + function getComplement(uint256 token) external view returns (uint256); + function getConditionId(uint256 token) external view returns (bytes32); + function getCtf() external view returns (address); + function getMaxFeeRate() external pure returns (uint256); + function getOrderStatus(bytes32 orderHash) external view returns (OrderStatus memory); + function getPolyProxyFactoryImplementation() external view returns (address); + function getPolyProxyWalletAddress(address _addr) external view returns (address); + function getProxyFactory() external view returns (address); + function getSafeAddress(address _addr) external view returns (address); + function getSafeFactory() external view returns (address); + function getSafeFactoryImplementation() external view returns (address); + function hashOrder(Order memory order) external view returns (bytes32); + function incrementNonce() external; + function isAdmin(address usr) external view returns (bool); + function isOperator(address usr) external view returns (bool); + function isValidNonce(address usr, uint256 nonce) external view returns (bool); + function matchOrders( + Order memory takerOrder, + Order[] memory makerOrders, + uint256 takerFillAmount, + uint256[] memory makerFillAmounts + ) external; + function nonces(address) external view returns (uint256); + function onERC1155BatchReceived(address, address, uint256[] memory, uint256[] memory, bytes memory) + external + returns (bytes4); + function onERC1155Received(address, address, uint256, uint256, bytes memory) external returns (bytes4); + function operators(address) external view returns (uint256); + function orderStatus(bytes32) external view returns (bool isFilledOrCancelled, uint256 remaining); + function parentCollectionId() external view returns (bytes32); + function pauseTrading() external; + function paused() external view returns (bool); + function proxyFactory() external view returns (address); + function registerToken(uint256 token, uint256 complement, bytes32 conditionId) external; + function registry(uint256) external view returns (uint256 complement, bytes32 conditionId); + function removeAdmin(address admin) external; + function removeOperator(address operator) external; + function renounceAdminRole() external; + function renounceOperatorRole() external; + function safeFactory() external view returns (address); + function setProxyFactory(address _newProxyFactory) external; + function setSafeFactory(address _newSafeFactory) external; + function supportsInterface(bytes4 interfaceId) external view returns (bool); + function unpauseTrading() external; + function validateComplement(uint256 token, uint256 complement) external view; + function validateOrder(Order memory order) external view; + function validateOrderSignature(bytes32 orderHash, Order memory order) external view; + function validateTokenId(uint256 tokenId) external view; +} diff --git a/src/interfaces/IFeeModule.sol b/src/interfaces/IFeeModule.sol new file mode 100644 index 0000000..69ab3b5 --- /dev/null +++ b/src/interfaces/IFeeModule.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {ICTFExchange} from "./ICTFExchange.sol"; + +interface IFeeModule { + event FeeRefunded(address token, address to, uint256 id, uint256 amount); + event FeeWithdrawn(address token, address to, uint256 id, uint256 amount); + event NewAdmin(address indexed admin, address indexed newAdminAddress); + event RemovedAdmin(address indexed admin, address indexed removedAdmin); + + function addAdmin(address admin) external; + function admins(address) external view returns (uint256); + function collateral() external view returns (address); + function ctf() external view returns (address); + function exchange() external view returns (address); + function isAdmin(address addr) external view returns (bool); + function matchOrders( + ICTFExchange.Order memory takerOrder, + ICTFExchange.Order[] memory makerOrders, + uint256 takerFillAmount, + uint256[] memory makerFillAmounts, + uint256 makerFeeRate + ) external; + function onERC1155BatchReceived(address, address, uint256[] memory, uint256[] memory, bytes memory) + external + returns (bytes4); + function onERC1155Received(address, address, uint256, uint256, bytes memory) external returns (bytes4); + function removeAdmin(address admin) external; + function renounceAdmin() external; + function withdrawFees(address to, uint256 id, uint256 amount) external; +} diff --git a/src/interfaces/INegRiskAdapter.sol b/src/interfaces/INegRiskAdapter.sol new file mode 100644 index 0000000..4547ac3 --- /dev/null +++ b/src/interfaces/INegRiskAdapter.sol @@ -0,0 +1,53 @@ +pragma solidity ^0.8.10; + +interface INegRiskAdapter { + event MarketPrepared(bytes32 indexed marketId, address indexed oracle, uint256 feeBips, bytes data); + event OutcomeReported(bytes32 indexed marketId, bytes32 indexed questionId, bool outcome); + event PayoutRedemption(address indexed redeemer, bytes32 indexed conditionId, uint256[] amounts, uint256 payout); + event PositionSplit(address indexed stakeholder, bytes32 indexed conditionId, uint256 amount); + event PositionsConverted( + address indexed stakeholder, bytes32 indexed marketId, uint256 indexed indexSet, uint256 amount + ); + event PositionsMerge(address indexed stakeholder, bytes32 indexed conditionId, uint256 amount); + event QuestionPrepared(bytes32 indexed marketId, bytes32 indexed questionId, uint256 index, bytes data); + + function FEE_DENOMINATOR() external view returns (uint256); + function NO_TOKEN_BURN_ADDRESS() external view returns (address); + function balanceOf(address _owner, uint256 _id) external view returns (uint256); + function balanceOfBatch(address[] memory _owners, uint256[] memory _ids) external view returns (uint256[] memory); + function col() external view returns (address); + function convertPositions(bytes32 _marketId, uint256 _indexSet, uint256 _amount) external; + function ctf() external view returns (address); + function getConditionId(bytes32 _questionId) external view returns (bytes32); + function getDetermined(bytes32 _marketId) external view returns (bool); + function getFeeBips(bytes32 _marketId) external view returns (uint256); + function getMarketData(bytes32 _marketId) external view returns (bytes32); + function getOracle(bytes32 _marketId) external view returns (address); + function getPositionId(bytes32 _questionId, bool _outcome) external view returns (uint256); + function getQuestionCount(bytes32 _marketId) external view returns (uint256); + function getResult(bytes32 _marketId) external view returns (uint256); + function mergePositions(address _collateralToken, bytes32, bytes32 _conditionId, uint256[] memory, uint256 _amount) + external; + function mergePositions(bytes32 _conditionId, uint256 _amount) external; + function onERC1155BatchReceived(address, address, uint256[] memory, uint256[] memory, bytes memory) + external + returns (bytes4); + function onERC1155Received(address, address, uint256, uint256, bytes memory) external returns (bytes4); + function prepareMarket(uint256 _feeBips, bytes memory _metadata) external returns (bytes32); + function prepareQuestion(bytes32 _marketId, bytes memory _metadata) external returns (bytes32); + function redeemPositions(bytes32 _conditionId, uint256[] memory _amounts) external; + function reportOutcome(bytes32 _questionId, bool _outcome) external; + function safeBatchTransferFrom( + address _from, + address _to, + uint256[] memory _ids, + uint256[] memory _values, + bytes memory _data + ) external; + function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes memory _data) external; + function splitPosition(address _collateralToken, bytes32, bytes32 _conditionId, uint256[] memory, uint256 _amount) + external; + function splitPosition(bytes32 _conditionId, uint256 _amount) external; + function vault() external view returns (address); + function wcol() external view returns (address); +} diff --git a/src/interfaces/Structs.sol b/src/interfaces/Structs.sol new file mode 100644 index 0000000..68900d6 --- /dev/null +++ b/src/interfaces/Structs.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +struct Order { + uint256 salt; + address maker; + address signer; + address taker; + uint256 tokenId; + uint256 makerAmount; + uint256 takerAmount; + uint256 expiration; + uint256 nonce; + uint256 feeRateBps; + uint8 side; + uint8 signatureType; + bytes signature; +} diff --git a/src/interfaces/index.sol b/src/interfaces/index.sol new file mode 100644 index 0000000..e3a18d1 --- /dev/null +++ b/src/interfaces/index.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity ^0.8.10; + +import {IAddressWhitelist} from "./IAddressWhitelist.sol"; +import {IAuth} from "./IAuth.sol"; +import {IConditionalTokens} from "./IConditionalTokens.sol"; +import {ICTFExchange} from "./ICTFExchange.sol"; +import {IERC20} from "./IERC20.sol"; +import {IFeeModule} from "./IFeeModule.sol"; +import {IFinder} from "./IFinder.sol"; +import {INegRiskAdapter} from "./INegRiskAdapter.sol"; +import {IOptimisticOracleV2} from "./IOptimisticOracleV2.sol"; +import {IUmaCtfAdapter} from "./IUmaCtfAdapter.sol"; diff --git a/src/snapshots/ConditionalTokens.snap.sol b/src/snapshots/ConditionalTokens.snap.sol index 29162de..505b263 100644 --- a/src/snapshots/ConditionalTokens.snap.sol +++ b/src/snapshots/ConditionalTokens.snap.sol @@ -15,7 +15,7 @@ contract NegRiskAdapterSnapshots is TestHelper, GasSnapshot { address oracle; function setUp() public { - oracle = _getAndLabelAddress("oracle"); + oracle = vm.createWallet("oracle").addr; ctf = IConditionalTokens(DeployLib.deployConditionalTokens()); usdc = new USDC(); } diff --git a/src/snapshots/NegRiskAdapter.snap.sol b/src/snapshots/NegRiskAdapter.snap.sol index 419b922..a9057df 100644 --- a/src/snapshots/NegRiskAdapter.snap.sol +++ b/src/snapshots/NegRiskAdapter.snap.sol @@ -19,8 +19,8 @@ contract NegRiskAdapterSnapshots is TestHelper, GasSnapshot { address vault; function setUp() public { - vault = _getAndLabelAddress("vault"); - oracle = _getAndLabelAddress("oracle"); + vault = vm.createWallet("vault").addr; + oracle = vm.createWallet("oracle").addr; ctf = IConditionalTokens(DeployLib.deployConditionalTokens()); usdc = new USDC(); nrAdapter = new NegRiskAdapter(address(ctf), address(usdc), vault); diff --git a/src/snapshots/WrappedCollateral.snap.sol b/src/snapshots/WrappedCollateral.snap.sol index c0d7bd6..fe7c2dc 100644 --- a/src/snapshots/WrappedCollateral.snap.sol +++ b/src/snapshots/WrappedCollateral.snap.sol @@ -16,7 +16,7 @@ contract WrappedCollateralSnapshots is TestHelper, GasSnapshot { function setUp() public { usdc = new USDC(); - owner = _getAndLabelAddress("owner"); + owner = vm.createWallet("owner").addr; uint8 decimals = usdc.decimals(); diff --git a/src/test/Integration.t.sol b/src/test/Integration.t.sol index 126e242..9bd9006 100644 --- a/src/test/Integration.t.sol +++ b/src/test/Integration.t.sol @@ -27,8 +27,8 @@ contract IntegrationTest is TestHelper { address admin; function setUp() public virtual { - vault = _getAndLabelAddress("vault"); - admin = _getAndLabelAddress("admin"); + vault = vm.createWallet("vault").addr; + admin = vm.createWallet("admin").addr; ctf = IConditionalTokens(DeployLib.deployConditionalTokens()); usdc = new USDC(); diff --git a/src/test/NegRiskAdapter/ERC1155Operations.t.sol b/src/test/NegRiskAdapter/ERC1155Operations.t.sol new file mode 100644 index 0000000..676b72b --- /dev/null +++ b/src/test/NegRiskAdapter/ERC1155Operations.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {console, NegRiskAdapter_SetUp} from "./NegRiskAdapterSetUp.sol"; +import {NegRiskIdLib} from "../../libraries/NegRiskIdLib.sol"; +import {IConditionalTokens} from "../../interfaces/IConditionalTokens.sol"; +import {StorageHelper} from "../../dev/StorageHelper.sol"; + +/// @title NegRiskAdapter_ERC1155Operations_Test +/// @notice test the ERC1155 proxy operations of the NegRiskAdapter +/// - safeTransferFrom +/// - balanceOf +contract NegRiskAdapter_ERC1155Operations_Test is NegRiskAdapter_SetUp, StorageHelper { + function setUp() public override { + super.setUp(); + } + + /*////////////////////////////////////////////////////////////// + SAFE TRANSFER FROM + //////////////////////////////////////////////////////////////*/ + + // only an admin can transfer, and they must additionally have approval + function test_ERC1155Operations_safeTransferFrom(uint256 _id, uint256 _value) public { + _dealERC1155(address(ctf), alice, _id, _value); + assertEq(ctf.balanceOf(alice, _id), _value); + + vm.prank(alice); + ctf.setApprovalForAll(address(nrAdapter), true); + + vm.prank(alice); + ctf.setApprovalForAll(admin, true); + + vm.prank(admin); + nrAdapter.safeTransferFrom(alice, brian, _id, _value, ""); + + assertEq(ctf.balanceOf(brian, _id), _value); + // proxied balanceOf + assertEq(nrAdapter.balanceOf(brian, _id), _value); + } + + // only an admin can transfer, and they must additionally have approval + function test_revert_ERC1155Operations_safeTransferFrom_notAdmin(uint256 _id, uint256 _value) public { + _dealERC1155(address(ctf), alice, _id, _value); + assertEq(ctf.balanceOf(alice, _id), _value); + + vm.prank(alice); + ctf.setApprovalForAll(address(nrAdapter), true); + + vm.prank(alice); + ctf.setApprovalForAll(carly, true); + + vm.expectRevert(NotAdmin.selector); + + vm.prank(carly); + nrAdapter.safeTransferFrom(alice, brian, _id, _value, ""); + } + + // only an admin can transfer, and they must additionally have approval + function test_revert_ERC1155Operations_safeTransferFrom_missingApproval1(uint256 _id, uint256 _value) public { + _dealERC1155(address(ctf), alice, _id, _value); + assertEq(ctf.balanceOf(alice, _id), _value); + + vm.prank(alice); + ctf.setApprovalForAll(address(nrAdapter), true); + + vm.expectRevert(NotApprovedForAll.selector); + + vm.prank(admin); + nrAdapter.safeTransferFrom(alice, brian, _id, _value, ""); + } + + // only an admin can transfer, and they must additionally have approval + function test_revert_ERC1155Operations_safeTransferFrom_missingApproval2(uint256 _id, uint256 _value) public { + _dealERC1155(address(ctf), alice, _id, _value); + assertEq(ctf.balanceOf(alice, _id), _value); + + vm.prank(alice); + ctf.setApprovalForAll(admin, true); + + vm.expectRevert("ERC1155: need operator approval for 3rd party transfers."); + + vm.prank(admin); + nrAdapter.safeTransferFrom(alice, brian, _id, _value, ""); + } +} diff --git a/src/test/NegRiskAdapter/NegRiskAdapterSetUp.sol b/src/test/NegRiskAdapter/NegRiskAdapterSetUp.sol index e9132d3..07f6c73 100644 --- a/src/test/NegRiskAdapter/NegRiskAdapterSetUp.sol +++ b/src/test/NegRiskAdapter/NegRiskAdapterSetUp.sol @@ -3,11 +3,11 @@ pragma solidity ^0.8.15; import {TestHelper, console} from "src/dev/TestHelper.sol"; -import {NegRiskAdapter, INegRiskAdapterEE} from "src/NegRiskAdapter.sol"; -import {WrappedCollateral} from "src/WrappedCollateral.sol"; -import {DeployLib} from "src/dev/libraries/DeployLib.sol"; -import {USDC} from "src/test/mock/USDC.sol"; -import {IConditionalTokens} from "src/interfaces/IConditionalTokens.sol"; +import {NegRiskAdapter, INegRiskAdapterEE} from "../../NegRiskAdapter.sol"; +import {WrappedCollateral} from "../../WrappedCollateral.sol"; +import {DeployLib} from "../../dev/libraries/DeployLib.sol"; +import {USDC} from "../../test/mock/USDC.sol"; +import {IConditionalTokens} from "../../interfaces/IConditionalTokens.sol"; contract NegRiskAdapter_SetUp is TestHelper, INegRiskAdapterEE { NegRiskAdapter nrAdapter; @@ -16,15 +16,19 @@ contract NegRiskAdapter_SetUp is TestHelper, INegRiskAdapterEE { IConditionalTokens ctf; address oracle; address vault; + address admin; uint256 constant FEE_BIPS_MAX = 10_000; function setUp() public virtual { - vault = _getAndLabelAddress("vault"); - oracle = _getAndLabelAddress("oracle"); + admin = vm.createWallet("admin").addr; + vault = vm.createWallet("vault").addr; + oracle = vm.createWallet("oracle").addr; ctf = IConditionalTokens(DeployLib.deployConditionalTokens()); usdc = new USDC(); nrAdapter = new NegRiskAdapter(address(ctf), address(usdc), vault); + NegRiskAdapter(nrAdapter).addAdmin(admin); + NegRiskAdapter(nrAdapter).renounceAdmin(); wcol = nrAdapter.wcol(); } } diff --git a/src/test/NegRiskCtfExchange/NegRiskCtfExchange.t.sol b/src/test/NegRiskCtfExchange/NegRiskCtfExchange.t.sol new file mode 100644 index 0000000..e5798be --- /dev/null +++ b/src/test/NegRiskCtfExchange/NegRiskCtfExchange.t.sol @@ -0,0 +1,417 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.15; + +import {console, Test} from "../../../lib/forge-std/src/Test.sol"; +import {Side} from "../../../lib/ctf-exchange/src/exchange/libraries/OrderStructs.sol"; +import {IConditionalTokens, ICTFExchange, IERC20, INegRiskAdapter} from "../../interfaces/index.sol"; +import {AddressLib} from "../../dev/libraries/AddressLib.sol"; +import {NegRiskCtfExchangeTestHelper} from "./NegRiskCtfExchangeTestHelper.sol"; + +contract NegRiskCtfExchange_Test is NegRiskCtfExchangeTestHelper { + function setUp() public { + marketId = INegRiskAdapter(negRiskAdapter).prepareMarket(0, "test_market"); + questionId = INegRiskAdapter(negRiskAdapter).prepareQuestion(marketId, "test_market"); + conditionId = INegRiskAdapter(negRiskAdapter).getConditionId(questionId); + + yesPositionId = INegRiskAdapter(negRiskAdapter).getPositionId(questionId, true); + noPositionId = INegRiskAdapter(negRiskAdapter).getPositionId(questionId, false); + + vm.prank(admin.addr); + ICTFExchange(negRiskCtfExchange).registerToken(yesPositionId, noPositionId, conditionId); + } + + function test_NegRiskCtfExchange_fillOrderBuy() public { + uint256 USDC_AMOUNT = 50_000_000; + uint256 TOKEN_AMOUNT = 100_000_000; + + // operator approvals + vm.startPrank(operator.addr); + IConditionalTokens(ctf).setApprovalForAll(negRiskCtfExchange, true); + IConditionalTokens(ctf).setApprovalForAll(negRiskAdapter, true); + vm.stopPrank(); + + // alice approvals + vm.prank(alice.addr); + IERC20(usdc).approve(negRiskCtfExchange, USDC_AMOUNT); + + // split initial tokens + vm.startPrank(carly.addr); + _dealERC20(usdc, carly.addr, TOKEN_AMOUNT); + IERC20(usdc).approve(negRiskAdapter, TOKEN_AMOUNT); + INegRiskAdapter(negRiskAdapter).splitPosition(usdc, bytes32(0), conditionId, partition, TOKEN_AMOUNT); + IConditionalTokens(ctf).safeTransferFrom(carly.addr, operator.addr, yesPositionId, TOKEN_AMOUNT, ""); + vm.stopPrank(); + + // deal alice USDC + _dealERC20(usdc, alice.addr, USDC_AMOUNT); + + // sign order + ICTFExchange.Order memory order = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: alice.privateKey, + _tokenId: yesPositionId, + _makerAmount: USDC_AMOUNT, + _takerAmount: TOKEN_AMOUNT, + _side: Side.BUY + }); + + // before + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(operator.addr, yesPositionId), TOKEN_AMOUNT); + assertEq(IERC20(usdc).balanceOf(alice.addr), USDC_AMOUNT); + assertEq(IERC20(usdc).balanceOf(operator.addr), 0); + + // -- FILL ORDER + vm.prank(operator.addr); + ICTFExchange(negRiskCtfExchange).fillOrder(order, USDC_AMOUNT); + + // after + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), TOKEN_AMOUNT); + assertEq(IConditionalTokens(ctf).balanceOf(operator.addr, yesPositionId), 0); + assertEq(IERC20(usdc).balanceOf(alice.addr), 0); + assertEq(IERC20(usdc).balanceOf(operator.addr), USDC_AMOUNT); + } + + function test_NegRiskCtfExchange_fillOrderSell() public { + uint256 USDC_AMOUNT = 50_000_000; + uint256 TOKEN_AMOUNT = 100_000_000; + + // operator approvals + vm.startPrank(operator.addr); + IERC20(usdc).approve(negRiskCtfExchange, USDC_AMOUNT); + vm.stopPrank(); + + // alice approvals + vm.startPrank(alice.addr); + IConditionalTokens(ctf).setApprovalForAll(negRiskCtfExchange, true); + IConditionalTokens(ctf).setApprovalForAll(negRiskAdapter, true); + vm.stopPrank(); + + // split initial tokens + vm.startPrank(carly.addr); + _dealERC20(usdc, carly.addr, TOKEN_AMOUNT); + IERC20(usdc).approve(negRiskAdapter, TOKEN_AMOUNT); + INegRiskAdapter(negRiskAdapter).splitPosition(usdc, bytes32(0), conditionId, partition, TOKEN_AMOUNT); + IConditionalTokens(ctf).safeTransferFrom(carly.addr, alice.addr, yesPositionId, TOKEN_AMOUNT, ""); + vm.stopPrank(); + + // deal operator USDC + _dealERC20(usdc, operator.addr, USDC_AMOUNT); + + // sign order + ICTFExchange.Order memory order = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: alice.privateKey, + _tokenId: yesPositionId, + _makerAmount: TOKEN_AMOUNT, + _takerAmount: USDC_AMOUNT, + _side: Side.SELL + }); + + // before + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), TOKEN_AMOUNT); + assertEq(IConditionalTokens(ctf).balanceOf(operator.addr, yesPositionId), 0); + assertEq(IERC20(usdc).balanceOf(alice.addr), 0); + assertEq(IERC20(usdc).balanceOf(operator.addr), USDC_AMOUNT); + + // -- FILL ORDER + vm.prank(operator.addr); + ICTFExchange(negRiskCtfExchange).fillOrder(order, TOKEN_AMOUNT); + + // after + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(operator.addr, yesPositionId), TOKEN_AMOUNT); + assertEq(IERC20(usdc).balanceOf(alice.addr), USDC_AMOUNT); + assertEq(IERC20(usdc).balanceOf(operator.addr), 0); + } + + function test_NegRiskCtfExchange_matchOrders_buySell() public { + uint256 USDC_AMOUNT = 50_000_000; + uint256 TOKEN_AMOUNT = 100_000_000; + + // alice approvals + vm.startPrank(alice.addr); + IERC20(usdc).approve(negRiskCtfExchange, TOKEN_AMOUNT); + vm.stopPrank(); + + // brian approvals + vm.startPrank(brian.addr); + IConditionalTokens(ctf).setApprovalForAll(negRiskCtfExchange, true); + IConditionalTokens(ctf).setApprovalForAll(negRiskAdapter, true); + vm.stopPrank(); + + // split initial tokens + usdc distribution + vm.startPrank(carly.addr); + _dealERC20(usdc, carly.addr, TOKEN_AMOUNT); + IERC20(usdc).approve(negRiskAdapter, TOKEN_AMOUNT); + INegRiskAdapter(negRiskAdapter).splitPosition(usdc, bytes32(0), conditionId, partition, TOKEN_AMOUNT); + // transfer yes tokens to brian + IConditionalTokens(ctf).safeTransferFrom(carly.addr, brian.addr, yesPositionId, TOKEN_AMOUNT, ""); + // deal USDC to alice + _dealERC20(usdc, alice.addr, USDC_AMOUNT); + vm.stopPrank(); + + // sign alice order + ICTFExchange.Order memory aliceOrder = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: alice.privateKey, + _tokenId: yesPositionId, + _makerAmount: USDC_AMOUNT, + _takerAmount: TOKEN_AMOUNT, + _side: Side.BUY + }); + + // sign brian order + ICTFExchange.Order memory brianOrder = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: brian.privateKey, + _tokenId: yesPositionId, + _makerAmount: TOKEN_AMOUNT, + _takerAmount: USDC_AMOUNT, + _side: Side.SELL + }); + + // takerOrder + takerFillAmount + ICTFExchange.Order memory takerOrder = aliceOrder; + uint256 takerFillAmount = USDC_AMOUNT; + + // makerOrders + makerFillAmounts + ICTFExchange.Order[] memory makerOrders = new ICTFExchange.Order[](1); + makerOrders[0] = brianOrder; + uint256[] memory makerFillAmounts = new uint256[](1); + makerFillAmounts[0] = TOKEN_AMOUNT; + + // before + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, yesPositionId), TOKEN_AMOUNT); + assertEq(IERC20(usdc).balanceOf(alice.addr), USDC_AMOUNT); + assertEq(IERC20(usdc).balanceOf(brian.addr), 0); + + // -- MATCH ORDERS -- + vm.prank(operator.addr); + ICTFExchange(negRiskCtfExchange).matchOrders(takerOrder, makerOrders, takerFillAmount, makerFillAmounts); + + // after + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), TOKEN_AMOUNT); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, yesPositionId), 0); + assertEq(IERC20(usdc).balanceOf(alice.addr), 0); + assertEq(IERC20(usdc).balanceOf(brian.addr), USDC_AMOUNT); + } + + function test_NegRiskCtfExchange_matchOrders_sellBuy() public { + // simply swap alice and brians orders + uint256 USDC_AMOUNT = 50_000_000; + uint256 TOKEN_AMOUNT = 100_000_000; + + // alice approvals + vm.startPrank(alice.addr); + IERC20(usdc).approve(negRiskCtfExchange, TOKEN_AMOUNT); + vm.stopPrank(); + + // brian approvals + vm.startPrank(brian.addr); + IConditionalTokens(ctf).setApprovalForAll(negRiskCtfExchange, true); + IConditionalTokens(ctf).setApprovalForAll(negRiskAdapter, true); + vm.stopPrank(); + + // split initial tokens + usdc distribution + vm.startPrank(carly.addr); + _dealERC20(usdc, carly.addr, TOKEN_AMOUNT); + IERC20(usdc).approve(negRiskAdapter, TOKEN_AMOUNT); + INegRiskAdapter(negRiskAdapter).splitPosition(usdc, bytes32(0), conditionId, partition, TOKEN_AMOUNT); + // transfer yes tokens to brian + IConditionalTokens(ctf).safeTransferFrom(carly.addr, brian.addr, yesPositionId, TOKEN_AMOUNT, ""); + // deal USDC to alice + _dealERC20(usdc, alice.addr, USDC_AMOUNT); + vm.stopPrank(); + + // sign alice order + ICTFExchange.Order memory aliceOrder = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: alice.privateKey, + _tokenId: yesPositionId, + _makerAmount: USDC_AMOUNT, + _takerAmount: TOKEN_AMOUNT, + _side: Side.BUY + }); + + // sign brian order + ICTFExchange.Order memory brianOrder = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: brian.privateKey, + _tokenId: yesPositionId, + _makerAmount: TOKEN_AMOUNT, + _takerAmount: USDC_AMOUNT, + _side: Side.SELL + }); + + // takerOrder + takerFillAmount + ICTFExchange.Order memory takerOrder = brianOrder; + uint256 takerFillAmount = TOKEN_AMOUNT; + + // makerOrders + makerFillAmounts + ICTFExchange.Order[] memory makerOrders = new ICTFExchange.Order[](1); + makerOrders[0] = aliceOrder; + uint256[] memory makerFillAmounts = new uint256[](1); + makerFillAmounts[0] = USDC_AMOUNT; + + // before + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, yesPositionId), TOKEN_AMOUNT); + assertEq(IERC20(usdc).balanceOf(alice.addr), USDC_AMOUNT); + assertEq(IERC20(usdc).balanceOf(brian.addr), 0); + + // -- MATCH ORDERS -- + vm.prank(operator.addr); + ICTFExchange(negRiskCtfExchange).matchOrders(takerOrder, makerOrders, takerFillAmount, makerFillAmounts); + + // after + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), TOKEN_AMOUNT); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, yesPositionId), 0); + assertEq(IERC20(usdc).balanceOf(alice.addr), 0); + assertEq(IERC20(usdc).balanceOf(brian.addr), USDC_AMOUNT); + } + + function test_NegRiskCtfExchange_matchOrders_buyBuy() public { + uint256 USDC_AMOUNT = 50_000_000; + uint256 TOKEN_AMOUNT = 100_000_000; + + // alice approvals + vm.startPrank(alice.addr); + IERC20(usdc).approve(negRiskCtfExchange, USDC_AMOUNT); + vm.stopPrank(); + + // brian approvals + vm.startPrank(brian.addr); + IERC20(usdc).approve(negRiskCtfExchange, USDC_AMOUNT); + vm.stopPrank(); + + // usdc distribution + _dealERC20(usdc, alice.addr, USDC_AMOUNT); + _dealERC20(usdc, brian.addr, USDC_AMOUNT); + + ICTFExchange.Order memory aliceOrder = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: alice.privateKey, + _tokenId: yesPositionId, + _makerAmount: USDC_AMOUNT, + _takerAmount: TOKEN_AMOUNT, + _side: Side.BUY + }); + + ICTFExchange.Order memory brianOrder = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: brian.privateKey, + _tokenId: noPositionId, + _makerAmount: USDC_AMOUNT, + _takerAmount: TOKEN_AMOUNT, + _side: Side.BUY + }); + + ICTFExchange.Order memory takerOrder = aliceOrder; + uint256 takerFillAmount = USDC_AMOUNT; + + ICTFExchange.Order[] memory makerOrders = new ICTFExchange.Order[](1); + makerOrders[0] = brianOrder; + uint256[] memory makerFillAmounts = new uint256[](1); + makerFillAmounts[0] = USDC_AMOUNT; + + // before + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, noPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, noPositionId), 0); + assertEq(IERC20(usdc).balanceOf(alice.addr), USDC_AMOUNT); + assertEq(IERC20(usdc).balanceOf(brian.addr), USDC_AMOUNT); + + // -- MATCH ORDERS -- + vm.prank(operator.addr); + ICTFExchange(negRiskCtfExchange).matchOrders(takerOrder, makerOrders, takerFillAmount, makerFillAmounts); + + // after + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), TOKEN_AMOUNT); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, noPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, noPositionId), TOKEN_AMOUNT); + assertEq(IERC20(usdc).balanceOf(alice.addr), 0); + assertEq(IERC20(usdc).balanceOf(brian.addr), 0); + } + + function test_NegRiskCtfExchange_matchOrders_sellSell() public { + uint256 USDC_AMOUNT = 50_000_000; + uint256 TOKEN_AMOUNT = 100_000_000; + + // alice approvals + vm.startPrank(alice.addr); + IConditionalTokens(ctf).setApprovalForAll(negRiskCtfExchange, true); + IConditionalTokens(ctf).setApprovalForAll(negRiskAdapter, true); + vm.stopPrank(); + + // brian approvals + vm.startPrank(brian.addr); + IConditionalTokens(ctf).setApprovalForAll(negRiskCtfExchange, true); + IConditionalTokens(ctf).setApprovalForAll(negRiskAdapter, true); + vm.stopPrank(); + + // split initial tokens + vm.startPrank(carly.addr); + _dealERC20(usdc, carly.addr, TOKEN_AMOUNT); + IERC20(usdc).approve(negRiskAdapter, TOKEN_AMOUNT); + INegRiskAdapter(negRiskAdapter).splitPosition(usdc, bytes32(0), conditionId, partition, TOKEN_AMOUNT); + // transfer yes tokens to alice + IConditionalTokens(ctf).safeTransferFrom(carly.addr, alice.addr, yesPositionId, TOKEN_AMOUNT, ""); + // transfer no tokens to brian + IConditionalTokens(ctf).safeTransferFrom(carly.addr, brian.addr, noPositionId, TOKEN_AMOUNT, ""); + vm.stopPrank(); + + // sign alice order + ICTFExchange.Order memory aliceOrder = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: alice.privateKey, + _tokenId: yesPositionId, + _makerAmount: TOKEN_AMOUNT, + _takerAmount: USDC_AMOUNT, + _side: Side.SELL + }); + + // sign brian order + ICTFExchange.Order memory brianOrder = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: brian.privateKey, + _tokenId: noPositionId, + _makerAmount: TOKEN_AMOUNT, + _takerAmount: USDC_AMOUNT, + _side: Side.SELL + }); + + // takerOrder + takerFillAmount + ICTFExchange.Order memory takerOrder = aliceOrder; + uint256 takerFillAmount = TOKEN_AMOUNT; + + // makerOrders + makerFillAmounts + ICTFExchange.Order[] memory makerOrders = new ICTFExchange.Order[](1); + makerOrders[0] = brianOrder; + uint256[] memory makerFillAmounts = new uint256[](1); + makerFillAmounts[0] = TOKEN_AMOUNT; + + // before + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), TOKEN_AMOUNT); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, noPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, noPositionId), TOKEN_AMOUNT); + assertEq(IERC20(usdc).balanceOf(alice.addr), 0); + assertEq(IERC20(usdc).balanceOf(brian.addr), 0); + + // -- MATCH ORDERS -- + vm.prank(operator.addr); + ICTFExchange(negRiskCtfExchange).matchOrders(takerOrder, makerOrders, takerFillAmount, makerFillAmounts); + + // after + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, noPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, noPositionId), 0); + assertEq(IERC20(usdc).balanceOf(alice.addr), USDC_AMOUNT); + assertEq(IERC20(usdc).balanceOf(brian.addr), USDC_AMOUNT); + } +} diff --git a/src/test/NegRiskCtfExchange/NegRiskCtfExchangeTestHelper.sol b/src/test/NegRiskCtfExchange/NegRiskCtfExchangeTestHelper.sol new file mode 100644 index 0000000..41e96a4 --- /dev/null +++ b/src/test/NegRiskCtfExchange/NegRiskCtfExchangeTestHelper.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {console, Test, Vm} from "../../../lib/forge-std/src/Test.sol"; +import {Side, SignatureType} from "../../../lib/ctf-exchange/src/exchange/libraries/OrderStructs.sol"; +import {NegRiskAdapter} from "../../NegRiskAdapter.sol"; +import {IConditionalTokens, ICTFExchange, IERC20} from "../../interfaces/index.sol"; +import {AddressLib} from "../../dev/libraries/AddressLib.sol"; +import {DeployLib} from "../../dev/libraries/DeployLib.sol"; +import {OrderHelper} from "../../dev/OrderHelper.sol"; +import {StorageHelper} from "../../dev/StorageHelper.sol"; +import {USDC} from "../mock/USDC.sol"; + +contract NegRiskCtfExchangeTestHelper is Test, OrderHelper, StorageHelper { + address immutable ctf; + address immutable negRiskAdapter; + address immutable negRiskCtfExchange; + address immutable usdc; + + Vm.Wallet alice; + Vm.Wallet brian; + Vm.Wallet carly; + Vm.Wallet admin; + Vm.Wallet operator; + + uint256[] partition; + + bytes32 marketId; + bytes32 questionId; + bytes32 conditionId; + + uint256 yesPositionId; + uint256 noPositionId; + + constructor() { + admin = vm.createWallet("admin"); + operator = vm.createWallet("operator"); + + alice = vm.createWallet("alice"); + brian = vm.createWallet("brian"); + carly = vm.createWallet("carly"); + + address vault = vm.createWallet("vault").addr; + + ctf = DeployLib.deployConditionalTokens(); + usdc = address(new USDC()); + + negRiskAdapter = address(new NegRiskAdapter(ctf, usdc, vault)); + negRiskCtfExchange = DeployLib.deployNegRiskCtfExchange({ + _collateral: usdc, + _ctf: ctf, + _negRiskAdapter: negRiskAdapter, + _proxyFactory: address(0), + _safeFactory: address(0) + }); + + // set initial admin + NegRiskAdapter(negRiskAdapter).addAdmin(admin.addr); + // allow negRiskCtfExchange to transfer using the NegRiskAdapter + NegRiskAdapter(negRiskAdapter).addAdmin(negRiskCtfExchange); + // renounce address(this) as admin + NegRiskAdapter(negRiskAdapter).renounceAdmin(); + + // set initial admin + ICTFExchange(negRiskCtfExchange).addAdmin(admin.addr); + // set operator + ICTFExchange(negRiskCtfExchange).addOperator(operator.addr); + + // renounce address(this) as admin and operator + ICTFExchange(negRiskCtfExchange).renounceAdminRole(); + ICTFExchange(negRiskCtfExchange).renounceOperatorRole(); + + partition = new uint256[](2); + partition[0] = 1; + partition[1] = 2; + } +} diff --git a/src/test/NegRiskFeeModule/NegRiskFeeModule.t.sol b/src/test/NegRiskFeeModule/NegRiskFeeModule.t.sol new file mode 100644 index 0000000..5291f66 --- /dev/null +++ b/src/test/NegRiskFeeModule/NegRiskFeeModule.t.sol @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {console, Test} from "../../../lib/forge-std/src/Test.sol"; +import {Side} from "../../../lib/ctf-exchange/src/exchange/libraries/OrderStructs.sol"; +import {IConditionalTokens, ICTFExchange, IERC20, IFeeModule, INegRiskAdapter} from "../../interfaces/index.sol"; +import {AddressLib} from "../../dev/libraries/AddressLib.sol"; +import {NegRiskFeeModuleTestHelper} from "./NegRiskFeeModuleTestHelper.sol"; + +contract NegRiskFeeModule_Test is NegRiskFeeModuleTestHelper { + function setUp() public { + marketId = INegRiskAdapter(negRiskAdapter).prepareMarket(0, "test_market"); + questionId = INegRiskAdapter(negRiskAdapter).prepareQuestion(marketId, "test_market"); + conditionId = INegRiskAdapter(negRiskAdapter).getConditionId(questionId); + + yesPositionId = INegRiskAdapter(negRiskAdapter).getPositionId(questionId, true); + noPositionId = INegRiskAdapter(negRiskAdapter).getPositionId(questionId, false); + + vm.prank(admin.addr); + ICTFExchange(negRiskCtfExchange).registerToken(yesPositionId, noPositionId, conditionId); + } + + function test_NegRiskFeeModule_withdrawFees(uint256 _id, uint256 _amount) public { + vm.assume(_id > 0); + _dealERC1155(ctf, negRiskFeeModule, _id, _amount); + + vm.prank(operator.addr); + IFeeModule(negRiskFeeModule).withdrawFees(alice.addr, _id, _amount); + + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, _id), _amount); + } + + function test_NegRiskFeeModule_matchOrders_buySell() public { + uint256 USDC_AMOUNT = 50_000_000; + uint256 TOKEN_AMOUNT = 100_000_000; + + // alice approvals + vm.startPrank(alice.addr); + IERC20(usdc).approve(negRiskCtfExchange, TOKEN_AMOUNT); + vm.stopPrank(); + + // brian approvals + vm.startPrank(brian.addr); + IConditionalTokens(ctf).setApprovalForAll(negRiskCtfExchange, true); + IConditionalTokens(ctf).setApprovalForAll(negRiskAdapter, true); + vm.stopPrank(); + + // split initial tokens + usdc distribution + vm.startPrank(carly.addr); + _dealERC20(usdc, carly.addr, TOKEN_AMOUNT); + IERC20(usdc).approve(negRiskAdapter, TOKEN_AMOUNT); + INegRiskAdapter(negRiskAdapter).splitPosition(usdc, bytes32(0), conditionId, partition, TOKEN_AMOUNT); + IConditionalTokens(ctf).setApprovalForAll(negRiskAdapter, true); + // deal USDC to alice + _dealERC20(usdc, alice.addr, USDC_AMOUNT); + // transfer yes tokens to brian + IConditionalTokens(ctf).safeTransferFrom(carly.addr, brian.addr, yesPositionId, TOKEN_AMOUNT, ""); + vm.stopPrank(); + + // sign alice order + ICTFExchange.Order memory aliceOrder = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: alice.privateKey, + _tokenId: yesPositionId, + _makerAmount: USDC_AMOUNT, + _takerAmount: TOKEN_AMOUNT, + _side: Side.BUY + }); + + // sign brian order + ICTFExchange.Order memory brianOrder = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: brian.privateKey, + _tokenId: yesPositionId, + _makerAmount: TOKEN_AMOUNT, + _takerAmount: USDC_AMOUNT, + _side: Side.SELL + }); + + // takerOrder + takerFillAmount + ICTFExchange.Order memory takerOrder = aliceOrder; + uint256 takerFillAmount = USDC_AMOUNT; + + // makerOrders + makerFillAmounts + ICTFExchange.Order[] memory makerOrders = new ICTFExchange.Order[](1); + makerOrders[0] = brianOrder; + uint256[] memory makerFillAmounts = new uint256[](1); + makerFillAmounts[0] = TOKEN_AMOUNT; + + // before + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, yesPositionId), TOKEN_AMOUNT); + assertEq(IERC20(usdc).balanceOf(alice.addr), USDC_AMOUNT); + assertEq(IERC20(usdc).balanceOf(brian.addr), 0); + + // -- MATCH ORDERS -- + vm.prank(operator.addr); + IFeeModule(negRiskFeeModule).matchOrders(takerOrder, makerOrders, takerFillAmount, makerFillAmounts, 0); + + // after + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), TOKEN_AMOUNT); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, yesPositionId), 0); + assertEq(IERC20(usdc).balanceOf(alice.addr), 0); + assertEq(IERC20(usdc).balanceOf(brian.addr), USDC_AMOUNT); + } + + function test_NegRiskFeeModule_matchOrders_sellBuy() public { + // simply swap alice and brians orders + uint256 USDC_AMOUNT = 50_000_000; + uint256 TOKEN_AMOUNT = 100_000_000; + + // alice approvals + vm.startPrank(alice.addr); + IERC20(usdc).approve(negRiskCtfExchange, TOKEN_AMOUNT); + vm.stopPrank(); + + // brian approvals + vm.startPrank(brian.addr); + IConditionalTokens(ctf).setApprovalForAll(negRiskCtfExchange, true); + IConditionalTokens(ctf).setApprovalForAll(negRiskAdapter, true); + vm.stopPrank(); + + // split initial tokens + usdc distribution + vm.startPrank(carly.addr); + _dealERC20(usdc, carly.addr, TOKEN_AMOUNT); + IERC20(usdc).approve(negRiskAdapter, TOKEN_AMOUNT); + IConditionalTokens(negRiskAdapter).splitPosition(usdc, bytes32(0), conditionId, partition, TOKEN_AMOUNT); + IConditionalTokens(ctf).setApprovalForAll(negRiskAdapter, true); + // deal USDC to alice + _dealERC20(usdc, alice.addr, USDC_AMOUNT); + // transfer yes tokens to brian + IConditionalTokens(ctf).safeTransferFrom(carly.addr, brian.addr, yesPositionId, TOKEN_AMOUNT, ""); + vm.stopPrank(); + + // sign alice order + ICTFExchange.Order memory aliceOrder = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: alice.privateKey, + _tokenId: yesPositionId, + _makerAmount: USDC_AMOUNT, + _takerAmount: TOKEN_AMOUNT, + _side: Side.BUY + }); + + // sign brian order + ICTFExchange.Order memory brianOrder = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: brian.privateKey, + _tokenId: yesPositionId, + _makerAmount: TOKEN_AMOUNT, + _takerAmount: USDC_AMOUNT, + _side: Side.SELL + }); + + // takerOrder + takerFillAmount + ICTFExchange.Order memory takerOrder = brianOrder; + uint256 takerFillAmount = TOKEN_AMOUNT; + + // makerOrders + makerFillAmounts + ICTFExchange.Order[] memory makerOrders = new ICTFExchange.Order[](1); + makerOrders[0] = aliceOrder; + uint256[] memory makerFillAmounts = new uint256[](1); + makerFillAmounts[0] = USDC_AMOUNT; + + // before + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, yesPositionId), TOKEN_AMOUNT); + assertEq(IERC20(usdc).balanceOf(alice.addr), USDC_AMOUNT); + assertEq(IERC20(usdc).balanceOf(brian.addr), 0); + + // -- MATCH ORDERS -- + vm.prank(operator.addr); + IFeeModule(negRiskFeeModule).matchOrders(takerOrder, makerOrders, takerFillAmount, makerFillAmounts, 0); + + // after + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), TOKEN_AMOUNT); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, yesPositionId), 0); + assertEq(IERC20(usdc).balanceOf(alice.addr), 0); + assertEq(IERC20(usdc).balanceOf(brian.addr), USDC_AMOUNT); + } + + function test_NegRiskFeeModule_matchOrders_buyBuy() public { + uint256 USDC_AMOUNT = 50_000_000; + uint256 TOKEN_AMOUNT = 100_000_000; + + // alice approvals + vm.startPrank(alice.addr); + IERC20(usdc).approve(negRiskCtfExchange, USDC_AMOUNT); + vm.stopPrank(); + + // brian approvals + vm.startPrank(brian.addr); + IERC20(usdc).approve(negRiskCtfExchange, USDC_AMOUNT); + vm.stopPrank(); + + // usdc distribution + _dealERC20(usdc, alice.addr, USDC_AMOUNT); + _dealERC20(usdc, brian.addr, USDC_AMOUNT); + + ICTFExchange.Order memory aliceOrder = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: alice.privateKey, + _tokenId: yesPositionId, + _makerAmount: USDC_AMOUNT, + _takerAmount: TOKEN_AMOUNT, + _side: Side.BUY + }); + + ICTFExchange.Order memory brianOrder = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: brian.privateKey, + _tokenId: noPositionId, + _makerAmount: USDC_AMOUNT, + _takerAmount: TOKEN_AMOUNT, + _side: Side.BUY + }); + + ICTFExchange.Order memory takerOrder = aliceOrder; + uint256 takerFillAmount = USDC_AMOUNT; + + ICTFExchange.Order[] memory makerOrders = new ICTFExchange.Order[](1); + makerOrders[0] = brianOrder; + uint256[] memory makerFillAmounts = new uint256[](1); + makerFillAmounts[0] = USDC_AMOUNT; + + // before + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, noPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, noPositionId), 0); + assertEq(IERC20(usdc).balanceOf(alice.addr), USDC_AMOUNT); + assertEq(IERC20(usdc).balanceOf(brian.addr), USDC_AMOUNT); + + // -- MATCH ORDERS -- + vm.prank(operator.addr); + IFeeModule(negRiskFeeModule).matchOrders(takerOrder, makerOrders, takerFillAmount, makerFillAmounts, 0); + + // after + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), TOKEN_AMOUNT); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, noPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, noPositionId), TOKEN_AMOUNT); + assertEq(IERC20(usdc).balanceOf(alice.addr), 0); + assertEq(IERC20(usdc).balanceOf(brian.addr), 0); + } + + function test_NegRiskFeeModule_matchOrders_sellSell() public { + uint256 USDC_AMOUNT = 50_000_000; + uint256 TOKEN_AMOUNT = 100_000_000; + + // alice approvals + vm.startPrank(alice.addr); + IConditionalTokens(ctf).setApprovalForAll(negRiskCtfExchange, true); + IConditionalTokens(ctf).setApprovalForAll(negRiskAdapter, true); + vm.stopPrank(); + + // brian approvals + vm.startPrank(brian.addr); + IConditionalTokens(ctf).setApprovalForAll(negRiskCtfExchange, true); + IConditionalTokens(ctf).setApprovalForAll(negRiskAdapter, true); + vm.stopPrank(); + + // split initial tokens + vm.startPrank(carly.addr); + _dealERC20(usdc, carly.addr, TOKEN_AMOUNT); + IERC20(usdc).approve(negRiskAdapter, TOKEN_AMOUNT); + INegRiskAdapter(negRiskAdapter).splitPosition(usdc, bytes32(0), conditionId, partition, TOKEN_AMOUNT); + // transfer yes tokens to alice + IConditionalTokens(ctf).safeTransferFrom(carly.addr, alice.addr, yesPositionId, TOKEN_AMOUNT, ""); + // transfer no tokens to brian + IConditionalTokens(ctf).safeTransferFrom(carly.addr, brian.addr, noPositionId, TOKEN_AMOUNT, ""); + vm.stopPrank(); + + // sign alice order + ICTFExchange.Order memory aliceOrder = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: alice.privateKey, + _tokenId: yesPositionId, + _makerAmount: TOKEN_AMOUNT, + _takerAmount: USDC_AMOUNT, + _side: Side.SELL + }); + + // sign brian order + ICTFExchange.Order memory brianOrder = _createAndSignOrder({ + _exchange: negRiskCtfExchange, + _pk: brian.privateKey, + _tokenId: noPositionId, + _makerAmount: TOKEN_AMOUNT, + _takerAmount: USDC_AMOUNT, + _side: Side.SELL + }); + + // takerOrder + takerFillAmount + ICTFExchange.Order memory takerOrder = aliceOrder; + uint256 takerFillAmount = TOKEN_AMOUNT; + + // makerOrders + makerFillAmounts + ICTFExchange.Order[] memory makerOrders = new ICTFExchange.Order[](1); + makerOrders[0] = brianOrder; + uint256[] memory makerFillAmounts = new uint256[](1); + makerFillAmounts[0] = TOKEN_AMOUNT; + + // before + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), TOKEN_AMOUNT); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, noPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, noPositionId), TOKEN_AMOUNT); + assertEq(IERC20(usdc).balanceOf(alice.addr), 0); + assertEq(IERC20(usdc).balanceOf(brian.addr), 0); + + // -- MATCH ORDERS -- + vm.prank(operator.addr); + IFeeModule(negRiskFeeModule).matchOrders(takerOrder, makerOrders, takerFillAmount, makerFillAmounts, 0); + + // after + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, yesPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(alice.addr, noPositionId), 0); + assertEq(IConditionalTokens(ctf).balanceOf(brian.addr, noPositionId), 0); + assertEq(IERC20(usdc).balanceOf(alice.addr), USDC_AMOUNT); + assertEq(IERC20(usdc).balanceOf(brian.addr), USDC_AMOUNT); + } +} diff --git a/src/test/NegRiskFeeModule/NegRiskFeeModuleTestHelper.sol b/src/test/NegRiskFeeModule/NegRiskFeeModuleTestHelper.sol new file mode 100644 index 0000000..dd14b78 --- /dev/null +++ b/src/test/NegRiskFeeModule/NegRiskFeeModuleTestHelper.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {console, Test, Vm} from "../../../lib/forge-std/src/Test.sol"; +import {Side, SignatureType} from "../../../lib/ctf-exchange/src/exchange/libraries/OrderStructs.sol"; +import {NegRiskAdapter} from "../../NegRiskAdapter.sol"; +import {IConditionalTokens, ICTFExchange, IERC20, IFeeModule} from "../../interfaces/index.sol"; +import {AddressLib} from "../../dev/libraries/AddressLib.sol"; +import {DeployLib} from "../../dev/libraries/DeployLib.sol"; +import {OrderHelper} from "../../dev/OrderHelper.sol"; +import {StorageHelper} from "../../dev/StorageHelper.sol"; +import {USDC} from "../mock/USDC.sol"; + +contract NegRiskFeeModuleTestHelper is Test, StorageHelper, OrderHelper { + address immutable ctf; + address immutable negRiskAdapter; + address immutable negRiskCtfExchange; + address immutable negRiskFeeModule; + address immutable usdc; + + Vm.Wallet alice; + Vm.Wallet brian; + Vm.Wallet carly; + Vm.Wallet admin; + Vm.Wallet operator; + + uint256[] partition; + + bytes32 marketId; + bytes32 questionId; + bytes32 conditionId; + + uint256 yesPositionId; + uint256 noPositionId; + + constructor() { + admin = vm.createWallet("admin"); + operator = vm.createWallet("operator"); + + alice = vm.createWallet("alice"); + brian = vm.createWallet("brian"); + carly = vm.createWallet("carly"); + + address vault = vm.createWallet("vault").addr; + + ctf = DeployLib.deployConditionalTokens(); + usdc = address(new USDC()); + + negRiskAdapter = address(new NegRiskAdapter(ctf, usdc, vault)); + negRiskCtfExchange = DeployLib.deployNegRiskCtfExchange({ + _collateral: usdc, + _ctf: ctf, + _negRiskAdapter: negRiskAdapter, + _proxyFactory: address(0), + _safeFactory: address(0) + }); + negRiskFeeModule = DeployLib.deployNegRiskFeeModule(negRiskCtfExchange, negRiskAdapter, ctf); + + /// NEG RISK ADAPTER + // set initial admin + NegRiskAdapter(negRiskAdapter).addAdmin(admin.addr); + // allow negRiskCtfExchange to transfer using the NegRiskAdapter + NegRiskAdapter(negRiskAdapter).addAdmin(negRiskCtfExchange); + // allow negRiskFeeModule to transfer using the NegRiskAdapter + NegRiskAdapter(negRiskAdapter).addAdmin(negRiskFeeModule); + + /// NEG RISK CTF EXCHANGE + // set initial admin + ICTFExchange(negRiskCtfExchange).addAdmin(admin.addr); + // set operator as operator + ICTFExchange(negRiskCtfExchange).addOperator(operator.addr); + // set fee module as operator + ICTFExchange(negRiskCtfExchange).addOperator(negRiskFeeModule); + /// NEG RISK FEE MODULE + IFeeModule(negRiskFeeModule).addAdmin(operator.addr); + + /// RENOUNCE + NegRiskAdapter(negRiskAdapter).renounceAdmin(); + IFeeModule(negRiskFeeModule).renounceAdmin(); + ICTFExchange(negRiskCtfExchange).renounceAdminRole(); + ICTFExchange(negRiskCtfExchange).renounceOperatorRole(); + + partition = new uint256[](2); + partition[0] = 1; + partition[1] = 2; + } +} diff --git a/src/test/NegRiskOperator.t.sol b/src/test/NegRiskOperator.t.sol index 41f236c..3b9a22e 100644 --- a/src/test/NegRiskOperator.t.sol +++ b/src/test/NegRiskOperator.t.sol @@ -24,8 +24,8 @@ contract NegRiskOperatorTest is TestHelper, INegRiskOperatorEE { uint256[] payoutsFalse = [0, 1]; function setUp() public { - vault = _getAndLabelAddress("vault"); - oracle = _getAndLabelAddress("oracle"); + vault = vm.createWallet("vault").addr; + oracle = vm.createWallet("oracle").addr; ctf = IConditionalTokens(DeployLib.deployConditionalTokens()); usdc = new USDC(); nrAdapter = new NegRiskAdapter(address(ctf), address(usdc), vault); diff --git a/src/test/WrappedCollateral.t.sol b/src/test/WrappedCollateral.t.sol index b7de52c..e6ce5a9 100644 --- a/src/test/WrappedCollateral.t.sol +++ b/src/test/WrappedCollateral.t.sol @@ -15,7 +15,7 @@ contract WrappedCollateralTest is TestHelper { function setUp() public { usdc = new USDC(); - owner = _getAndLabelAddress("owner"); + owner = vm.createWallet("owner").addr; uint8 decimals = usdc.decimals(); diff --git a/src/test/dev/StorageHelper.t.sol b/src/test/dev/StorageHelper.t.sol new file mode 100644 index 0000000..364139d --- /dev/null +++ b/src/test/dev/StorageHelper.t.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {console, Test} from "../../../lib/forge-std/src/Test.sol"; +import {StorageHelper} from "../../dev/StorageHelper.sol"; +import {DeployLib} from "../../dev/libraries/DeployLib.sol"; +import {IConditionalTokens} from "../../interfaces/IConditionalTokens.sol"; + +contract StorageHelper_Test is Test, StorageHelper { + function test_Storage_ERC1155Balances(uint256 _id, uint256 _amount) public { + address alice = vm.createWallet("alice").addr; + address ctf = DeployLib.deployConditionalTokens(); + _dealERC1155(ctf, alice, _id, _amount); + + assertEq(IConditionalTokens(ctf).balanceOf(alice, _id), _amount); + } +}