diff --git a/.gitmodules b/.gitmodules index a20ca34..c551dcf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "lib/safe-smart-account"] path = lib/safe-smart-account url = https://github.com/safe-global/safe-smart-account +[submodule "lib/contracts"] + path = lib/contracts + url = https://github.com/cowprotocol/contracts diff --git a/lib/contracts b/lib/contracts new file mode 160000 index 0000000..ba57381 --- /dev/null +++ b/lib/contracts @@ -0,0 +1 @@ +Subproject commit ba57381759aa1d3f68bb18a080907c4a0045dadd diff --git a/remappings.txt b/remappings.txt index 66bbf10..4a6512c 100644 --- a/remappings.txt +++ b/remappings.txt @@ -8,3 +8,4 @@ halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/ openzeppelin-contracts/=lib/openzeppelin-contracts/ safe-smart-account/=lib/safe-smart-account/ solmate/=lib/solmate/src/ +@cowprotocol/contracts/=lib/contracts/src/contracts \ No newline at end of file diff --git a/script/DeployFactory.s.sol b/script/DeployFactory.s.sol new file mode 100644 index 0000000..5ee6118 --- /dev/null +++ b/script/DeployFactory.s.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.28; + +import {Script, console} from "forge-std/Script.sol"; +import {CirclesBackingFactory} from "src/factory/CirclesBackingFactory.sol"; + +contract DeployFactory is Script { + address deployer = address(0x6BF173798733623cc6c221eD52c010472247d861); + CirclesBackingFactory public circlesBackingFactory; // 0xD608978aD1e1473fa98BaD368e767C5b11e3b3cE + + function setUp() public {} + + function run() public { + vm.startBroadcast(deployer); + + circlesBackingFactory = new CirclesBackingFactory(deployer, 1); + + vm.stopBroadcast(); + console.log(address(circlesBackingFactory), "CirclesBackingFactory"); + } +} + +/* +curl -X 'POST' \ + 'https://api.cow.fi/xdai/api/v1/orders' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "sellToken": "0x2a22f9c3b484c3629090FeED35F17Ff8F88f76F0", + "buyToken": "0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1", + "receiver": "0xe75F06c807038D7D38e4f9716FF953eA1dA39157", + "sellAmount": "1000000", + "buyAmount": "1", + "validTo": 1894324190, + "feeAmount": "0", + "kind": "sell", + "partiallyFillable": false, + "sellTokenBalance": "erc20", + "buyTokenBalance": "erc20", + "signingScheme": "presign", + "signature": "0x", + "from": "0xe75F06c807038D7D38e4f9716FF953eA1dA39157", + "appData": "{\"version\":\"1.1.0\",\"appCode\":\"Circles backing powered by AboutCircles\",\"metadata\":{\"hooks\":{\"version\":\"0.1.0\",\"post\":[{\"target\":\"0xe75f06c807038d7d38e4f9716ff953ea1da39157\",\"callData\":\"0x13e8f89f\",\"gasLimit\":\"6000000\"}]}}}" +}' +*/ diff --git a/script/DeployMintPolicy.s.sol b/script/DeployMintPolicy.s.sol index 1e8b3a7..aa55713 100644 --- a/script/DeployMintPolicy.s.sol +++ b/script/DeployMintPolicy.s.sol @@ -2,20 +2,20 @@ pragma solidity ^0.8.28; import {Script, console} from "forge-std/Script.sol"; -import {TestLBPMintPolicy} from "src/policy/TestLBPMintPolicy.sol"; import {CreateTestProxyLBPMintPolicy} from "src/proxy/CreateTestProxyLBPMintPolicy.sol"; +import {MintPolicy} from "circles-contracts-v2/groups/BaseMintPolicy.sol"; contract DeployMintPolicy is Script { address deployer = address(0x6BF173798733623cc6c221eD52c010472247d861); - TestLBPMintPolicy public mintPolicy; // 0xCb10eC7A4D9D764b1DcfcB9c2EBa675B1e756C96 - CreateTestProxyLBPMintPolicy public proxyDeployer; // 0x777f78921890Df5Db755e77CbA84CBAdA5DB56D2 + MintPolicy public mintPolicy; + CreateTestProxyLBPMintPolicy public proxyDeployer; function setUp() public {} function run() public { vm.startBroadcast(deployer); - mintPolicy = new TestLBPMintPolicy(); + mintPolicy = new MintPolicy(); proxyDeployer = new CreateTestProxyLBPMintPolicy(address(mintPolicy)); vm.stopBroadcast(); diff --git a/script/DeployModuleFactory.s.sol b/script/DeployModuleFactory.s.sol deleted file mode 100644 index f8970ef..0000000 --- a/script/DeployModuleFactory.s.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity ^0.8.28; - -import {Script, console} from "forge-std/Script.sol"; -import {TestTrustModule} from "src/module/TestTrustModule.sol"; -import {TestCirclesLBPFactory} from "src/factory/TestCirclesLBPFactory.sol"; - -contract DeployModuleFactory is Script { - address deployer = address(0x6BF173798733623cc6c221eD52c010472247d861); - TestTrustModule public trustModule; // 0x56652E53649F20C6a360Ea5F25379F9987cECE82 - TestCirclesLBPFactory public circlesLBPFactory; // 0x97030b525248cAc78aabcc33D37139BfB5a34750 - - function setUp() public {} - - function run() public { - vm.startBroadcast(deployer); - - trustModule = new TestTrustModule(); - circlesLBPFactory = new TestCirclesLBPFactory(); - - vm.stopBroadcast(); - console.log(address(trustModule), "TrustModule"); - console.log(address(circlesLBPFactory), "CirclesLBPFactory"); - } -} diff --git a/script/DeployOrderCreator.s.sol b/script/DeployOrderCreator.s.sol new file mode 100644 index 0000000..fcb1b5e --- /dev/null +++ b/script/DeployOrderCreator.s.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.28; + +import {Script, console} from "forge-std/Script.sol"; +import {OrderCreator} from "src/prototype/OrderCreator.sol"; + +contract DeployPrototype is Script { + address deployer = address(0x6BF173798733623cc6c221eD52c010472247d861); + OrderCreator public orderCreator; + + function setUp() public {} + + function run() public { + vm.startBroadcast(deployer); + + orderCreator = new OrderCreator(); + orderCreator.createOrder(); + + vm.stopBroadcast(); + console.log(address(orderCreator), "orderCreator"); + } +} diff --git a/src/CirclesBacking.sol b/src/CirclesBacking.sol new file mode 100644 index 0000000..b946be3 --- /dev/null +++ b/src/CirclesBacking.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.28; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ICowswapSettlement} from "src/interfaces/ICowswapSettlement.sol"; +import {IFactory} from "src/interfaces/IFactory.sol"; +import {ILBP} from "src/interfaces/ILBP.sol"; +import {IVault} from "src/interfaces/IVault.sol"; + +contract CirclesBacking { + // Errors + /// Already initialized. + error AlreadyInitialized(); + /// Function must be called only by Cowswap posthook. + error OrderNotFilledYet(); + /// LBP is already created. + error AlreadyCreated(); + /// Cowswap solver must transfer the swap result before calling posthook. + error InsufficientBackingAssetBalance(); + /// Unauthorized access. + error NotBacker(); + /// Balancer Pool Tokens are still locked until `timestamp`. + error TokensLockedUntilTimestamp(uint256 timestamp); + + // Events + /// @notice Emitted when Cowswap order is created, logging order uid. + event OrderCreated(bytes orderUid); + + // Constants + /// @notice Gnosis Protocol v2 Settlement Contract. + ICowswapSettlement public constant COWSWAP_SETTLEMENT = + ICowswapSettlement(address(0x9008D19f58AAbD9eD0D60971565AA8510560ab41)); + /// @notice Gnosis Protocol v2 Vault Relayer Contract. + address public constant VAULT_RELAY = 0xC92E8bdf79f0507f65a392b0ab4667716BFE0110; + /// @dev Circles Backing Factory. + IFactory internal immutable FACTORY; + /// @dev LBP token weight 50%. + uint256 internal constant WEIGHT_50 = 0.5 ether; + /// @dev Update weight duration and lbp lock period is set to 1 year. + uint256 internal constant UPDATE_WEIGHT_DURATION = 365 days; + + // Storage + /// @notice Address of circles avatar, which has backed his personal circles. + address public backer; + /// @notice Address of one of supported assets, which was used to back circles. + address public backingAsset; + /// @notice Address of ERC20 stable circles version (InflationaryCircles), which is used as underlying asset in lbp. + address public personalCircles; + /// @notice Address of created Liquidity Bootstrapping Pool, which represents backing liquidity. + address public lbp; + uint256 stableCirclesAmount; + /// @notice Timestamp, when locked balancer pool tokens are allowed to be claimed by backer. + uint256 public balancerPoolTokensUnlockTimestamp; + /// @notice Cowswap order uid. + bytes public storedOrderUid; + + constructor() { + FACTORY = IFactory(msg.sender); + } + + // Backing logic + + /// @notice Initiates core values and backing process, approves Cowswap to spend USDC and presigns order. + function initiateBacking( + address _backer, + address _backingAsset, + address _personalCircles, + bytes memory orderUid, + address usdc, + uint256 tradeAmount, + uint256 stableCRCAmount + ) external { + if (backer != address(0)) revert AlreadyInitialized(); + // init + backer = _backer; + backingAsset = _backingAsset; + personalCircles = _personalCircles; + stableCirclesAmount = stableCRCAmount; + + // Approve USDC to Vault Relay contract + IERC20(usdc).approve(VAULT_RELAY, tradeAmount); + + // Store the order UID + storedOrderUid = orderUid; + + // Place the order using "setPreSignature" + COWSWAP_SETTLEMENT.setPreSignature(orderUid, true); + + // Emit event with the order UID + emit OrderCreated(orderUid); + } + + /// @notice Method, which should be used as Cowswap posthook interaction. + /// Creates preconfigured LBP and provides liquidity to it. + function createLBP() external { + // Check if the order has been filled on the CowSwap settlement contract + uint256 filledAmount = COWSWAP_SETTLEMENT.filledAmount(storedOrderUid); + if (filledAmount == 0) revert OrderNotFilledYet(); + if (lbp != address(0)) revert AlreadyCreated(); + + // Backing asset balance of the contract + uint256 backingAssetBalance = IERC20(backingAsset).balanceOf(address(this)); + if (backingAssetBalance == 0) revert InsufficientBackingAssetBalance(); + + // Create LBP + bytes32 poolId; + IVault.JoinPoolRequest memory request; + address vault; + (lbp, poolId, request, vault) = + FACTORY.createLBP(personalCircles, stableCirclesAmount, backingAsset, backingAssetBalance); + + // approve vault + IERC20(personalCircles).approve(vault, stableCirclesAmount); + IERC20(backingAsset).approve(vault, backingAssetBalance); + + // provide liquidity into lbp + IVault(vault).joinPool( + poolId, + address(this), // sender + address(this), // recipient + request + ); + + // update weight gradually + uint256 timestampInYear = block.timestamp + UPDATE_WEIGHT_DURATION; + ILBP(lbp).updateWeightsGradually(block.timestamp, timestampInYear, _endWeights()); + + // set bpt unlock + balancerPoolTokensUnlockTimestamp = timestampInYear; + } + + // Balancer pool tokens + + /// @notice Method allows backer to claim balancer pool tokens after lock period or in case of global release. + /// @param receiver Address, which will receive balancer pool tokens. + function releaseBalancerPoolTokens(address receiver) external { + if (msg.sender != backer) revert NotBacker(); + + if (FACTORY.releaseTimestamp() > uint32(block.timestamp)) { + if (balancerPoolTokensUnlockTimestamp > block.timestamp) { + revert TokensLockedUntilTimestamp(balancerPoolTokensUnlockTimestamp); + } + } + // zeroed timestamp + balancerPoolTokensUnlockTimestamp = 0; + + uint256 bptAmount = IERC20(lbp).balanceOf(address(this)); + IERC20(lbp).transfer(receiver, bptAmount); + } + + // Internal functions + + function _endWeights() internal pure returns (uint256[] memory endWeights) { + endWeights = new uint256[](2); + endWeights[0] = WEIGHT_50; + endWeights[1] = WEIGHT_50; + } +} diff --git a/src/factory/CirclesBackingFactory.sol b/src/factory/CirclesBackingFactory.sol new file mode 100644 index 0000000..d01aa7c --- /dev/null +++ b/src/factory/CirclesBackingFactory.sol @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.28; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IGetUid} from "src/interfaces/IGetUid.sol"; +import {IVault} from "src/interfaces/IVault.sol"; +import {INoProtocolFeeLiquidityBootstrappingPoolFactory} from "src/interfaces/ILBPFactory.sol"; +import {ILBP} from "src/interfaces/ILBP.sol"; +import {IHub} from "src/interfaces/IHub.sol"; +import {ILiftERC20} from "src/interfaces/ILiftERC20.sol"; +import {CirclesBacking} from "src/CirclesBacking.sol"; + +/** + * @title Circles Backing Factory. + * @notice Contract allows to create CircleBacking instances. + * Administrates supported backing assets and global balancer pool tokens release. + */ +contract CirclesBackingFactory { + // Errors + /// Only HubV2 allowed to call. + error OnlyHub(); + /// Received CRC amount is `received`, required CRC amount is `required`. + error NotExactlyRequiredCRCAmount(uint256 required, uint256 received); + /// Backing is allowed only for Hub human avatars. + error OnlyHumanAvatarsAreSupported(); + /// Backing in favor is dissalowed. Back only your personal CRC. + error BackingInFavorDissalowed(); + /// Circles backing does not support `requestedAsset` asset. + error UnsupportedBackingAsset(address requestedAsset); + /// Deployment of CirclesBacking instance initiated by user `backer` has failed. + error CirclesBackingDeploymentFailed(address backer); + /// Missing approval of this address to spend personal CRC. + error PersonalCirclesApprovalIsMissing(); + /// Method can be called only by instance of CirclesBacking deployed by this factory. + error OnlyCirclesBacking(); + /// Unauthorized access. + error NotAdmin(); + /// Exit Liquidity Bootstraping Pool supports only two tokens pools. + error OnlyTwoTokenLBPSupported(); + + // Events + /// @notice Emitted when a CirclesBacking is created. + event CirclesBackingDeployed(address indexed backer, address indexed circlesBackingInstance); + /// @notice Emitted when a LBP is created. + event LBPDeployed(address indexed circlesBackingInstance, address indexed lbp); + /// @notice Emitted when a Circles backing process is initiated. + event CirclesBackingInitiated( + address indexed backer, + address indexed circlesBackingInstance, + address backingAsset, + address personalCirclesAddress + ); + /// @notice Emitted when a Circles backing process is completed. + event CirclesBackingCompleted(address indexed backer, address indexed circlesBackingInstance, address lbp); + + // Constants + /// @notice Address allowed to set supported backing assets and global bpt release timestamp. + address public immutable ADMIN; + + // Cowswap order constants. + /// @notice Helper contract for crafting Uid. + IGetUid public constant GET_UID_CONTRACT = IGetUid(address(0xCA51403B524dF7dA6f9D6BFc64895AD833b5d711)); + /// @notice USDC.e contract address. + address public constant USDC = 0x2a22f9c3b484c3629090FeED35F17Ff8F88f76F0; + /// @notice ERC20 decimals value for USDC.e. + uint256 public constant USDC_DECIMALS = 1e6; + /// @notice Amount of USDC.e to use in a swap for backing asset. + uint256 public immutable TRADE_AMOUNT; + /// @notice Deadline for orders expiration - set as timestamp in 5 years after deployment. + uint32 public immutable VALID_TO; + /// @notice Order appdata divided into 2 strings to insert deployed instance address. + string public constant preAppData = + '{"version":"1.1.0","appCode":"Circles backing powered by AboutCircles","metadata":{"hooks":{"version":"0.1.0","post":[{"target":"'; + string public constant postAppData = '","callData":"0x13e8f89f","gasLimit":"6000000"}]}}}'; // Updated calldata and gaslimit for createLBP + + /// LBP constants. + /// @notice Balancer v2 Vault. + address public constant VAULT = address(0xBA12222222228d8Ba445958a75a0704d566BF2C8); + /// @notice Balancer v2 LBPFactory. + INoProtocolFeeLiquidityBootstrappingPoolFactory public constant LBP_FACTORY = + INoProtocolFeeLiquidityBootstrappingPoolFactory(address(0x85a80afee867aDf27B50BdB7b76DA70f1E853062)); + /// @dev LBP token weight 1%. + uint256 internal constant WEIGHT_1 = 0.01 ether; + /// @dev LBP token weight 99%. + uint256 internal constant WEIGHT_99 = 0.99 ether; + /// @dev Swap fee percentage is set to 1%. + uint256 internal constant SWAP_FEE = 0.01 ether; + /// @dev BPT name and symbol prefix. + string internal constant LBP_PREFIX = "circlesBackingLBP-"; + + // Circles constants + /// @notice Circles Hub v2. + IHub public constant HUB_V2 = IHub(address(0xc12C1E50ABB450d6205Ea2C3Fa861b3B834d13e8)); + /// @notice Circles v2 LiftERC20 contract. + ILiftERC20 public constant LIFT_ERC20 = ILiftERC20(address(0x5F99a795dD2743C36D63511f0D4bc667e6d3cDB5)); + /// @notice Amount of InflationaryCircles to use in LBP initial liquidity. + uint256 public constant CRC_AMOUNT = 48 ether; + + // Storage + /// @notice Stores supported assets. + mapping(address supportedAsset => bool) public supportedBackingAssets; + /// @notice Links CirclesBacking instances to their creators. + mapping(address circlesBacking => address backer) public backerOf; + /// @notice Links backer to his CirclesBacking. + mapping(address backer => address circlesBacking) public circlesBackingOf; + /// @notice Global release timestamp for balancer pool tokens. + uint32 public releaseTimestamp = type(uint32).max; + + // Modifiers + modifier onlyAdmin() { + if (msg.sender != ADMIN) revert NotAdmin(); + _; + } + + /** + * @dev Reentrancy guard for nonReentrant functions. + * see https://soliditylang.org/blog/2024/01/26/transient-storage/ + */ + modifier nonReentrant() { + assembly { + if tload(0) { revert(0, 0) } + tstore(0, 1) + } + _; + assembly { + tstore(0, 0) + } + } + + // Constructor + constructor(address admin, uint256 usdcInteger) { + ADMIN = admin; + TRADE_AMOUNT = usdcInteger * USDC_DECIMALS; + VALID_TO = uint32(block.timestamp + 1825 days); + supportedBackingAssets[address(0x8e5bBbb09Ed1ebdE8674Cda39A0c169401db4252)] = true; // WBTC + supportedBackingAssets[address(0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1)] = true; // WETH + supportedBackingAssets[address(0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb)] = true; // GNO + supportedBackingAssets[address(0xaf204776c7245bF4147c2612BF6e5972Ee483701)] = true; // sDAI + } + + // Admin logic + + /// @notice Method sets global release timestamp for unlocking balancer pool tokens. + function setReleaseTimestamp(uint32 timestamp) external onlyAdmin { + releaseTimestamp = timestamp; + } + + /// @notice Method sets supported status for backing asset. + function setSupportedBackingAssetStatus(address backingAsset, bool status) external onlyAdmin { + supportedBackingAssets[backingAsset] = status; + } + + // Backing logic + + /// @dev Required upfront approval of this contract for USDC.e + /// @dev Is called inside onERC1155Received callback by Hub call Circles 1155 transferFrom. + function startBacking(address backer, address backingAsset, address stableCRCAddress, uint256 stableCRCAmount) + internal + { + if (!supportedBackingAssets[backingAsset]) revert UnsupportedBackingAsset(backingAsset); + + address instance = deployCirclesBacking(backer); + + // transfer USDC.e + IERC20(USDC).transferFrom(backer, instance, TRADE_AMOUNT); + // transfer stable circles + IERC20(stableCRCAddress).transfer(instance, stableCRCAmount); + + // create order + (, bytes32 appData) = getAppData(instance); + // Generate order UID using the "getUid" contract + (bytes32 orderDigest,) = GET_UID_CONTRACT.getUid( + USDC, // sellToken + backingAsset, // buyToken + instance, // receiver + TRADE_AMOUNT, // sellAmount + 1, // buyAmount: Determined by off-chain logic or Cowswap solvers + VALID_TO, // order expiry + appData, // appData hash + 0, // FeeAmount + true, // IsSell + false // PartiallyFillable + ); + // Construct the order UID + bytes memory orderUid = abi.encodePacked(orderDigest, instance, uint32(VALID_TO)); + // Initiate backing + CirclesBacking(instance).initiateBacking( + backer, backingAsset, stableCRCAddress, orderUid, USDC, TRADE_AMOUNT, stableCRCAmount + ); + emit CirclesBackingInitiated(backer, instance, backingAsset, stableCRCAddress); + } + + // LBP logic + + /// @notice Creates LBP with underlying assets: `backingAssetAmount` backingAsset(`backingAsset`) and `CRC_AMOUNT` InflationaryCircles(`personalCRC`). + /// @param personalCRC Address of InflationaryCircles (stable ERC20) used as underlying asset in lbp. + /// @param backingAsset Address of backing asset used as underlying asset in lbp. + /// @param backingAssetAmount Amount of backing asset used in lbp. + function createLBP(address personalCRC, uint256 personalCRCAmount, address backingAsset, uint256 backingAssetAmount) + external + returns (address lbp, bytes32 poolId, IVault.JoinPoolRequest memory request, address vault) + { + address backer = backerOf[msg.sender]; + if (backer == address(0)) revert OnlyCirclesBacking(); + + // prepare inputs + IERC20[] memory tokens = new IERC20[](2); + bool tokenZero = personalCRC < backingAsset; + tokens[0] = tokenZero ? IERC20(personalCRC) : IERC20(backingAsset); + tokens[1] = tokenZero ? IERC20(backingAsset) : IERC20(personalCRC); + + uint256[] memory weights = new uint256[](2); + weights[0] = tokenZero ? WEIGHT_1 : WEIGHT_99; + weights[1] = tokenZero ? WEIGHT_99 : WEIGHT_1; + + // create LBP + lbp = LBP_FACTORY.create( + _name(personalCRC), + _symbol(personalCRC), + tokens, + weights, + SWAP_FEE, + msg.sender, // lbp owner + true // enable swap on start + ); + + emit LBPDeployed(msg.sender, lbp); + + poolId = ILBP(lbp).getPoolId(); + + uint256[] memory amountsIn = new uint256[](2); + amountsIn[0] = tokenZero ? personalCRCAmount : backingAssetAmount; + amountsIn[1] = tokenZero ? backingAssetAmount : personalCRCAmount; + + bytes memory userData = abi.encode(ILBP.JoinKind.INIT, amountsIn); + + request = IVault.JoinPoolRequest(tokens, amountsIn, userData, false); + vault = VAULT; + + emit CirclesBackingCompleted(backer, msg.sender, lbp); + } + + /// @notice General wrapper function over vault.exitPool, allows to extract + /// liquidity from pool by approving this Factory to spend Balancer Pool Tokens. + /// @dev Required Balancer Pool Token approval for bptAmount before call + function exitLBP(address lbp, uint256 bptAmount) external { + // transfer bpt tokens from msg.sender + IERC20(lbp).transferFrom(msg.sender, address(this), bptAmount); + + uint256[] memory minAmountsOut = new uint256[](2); + minAmountsOut[0] = uint256(0); + minAmountsOut[1] = uint256(0); + + bytes32 poolId = ILBP(lbp).getPoolId(); + + (IERC20[] memory poolTokens,,) = IVault(VAULT).getPoolTokens(poolId); + if (poolTokens.length != minAmountsOut.length) revert OnlyTwoTokenLBPSupported(); + + // exit pool + IVault(VAULT).exitPool( + poolId, + address(this), // sender + payable(msg.sender), // recipient + IVault.ExitPoolRequest( + poolTokens, minAmountsOut, abi.encode(ILBP.ExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT, bptAmount), false + ) + ); + } + + // View functions + + // counterfactual + /** + * @notice Computes the deterministic address for CirclesBacking contract. + * @param backer Address which is backing circles. + * @return predictedAddress Predicted address of the deployed contract. + */ + function computeAddress(address backer) external view returns (address predictedAddress) { + bytes32 salt = keccak256(abi.encodePacked(backer)); + bytes memory bytecode = type(CirclesBacking).creationCode; + predictedAddress = address( + uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(bytecode))))) + ); + } + + // cowswap app data + /// @notice Returns stringified json and its hash representing app data for Cowswap. + function getAppData(address _circlesBackingInstance) + public + pure + returns (string memory appDataString, bytes32 appDataHash) + { + string memory instanceAddressStr = addressToString(_circlesBackingInstance); + appDataString = string.concat(preAppData, instanceAddressStr, postAppData); + appDataHash = keccak256(bytes(appDataString)); + } + + // personal circles + /// @notice Returns address of avatar InflationaryCircles. + /// @dev this call will revert, if avatar is not registered as human or group in Hub contract + function getPersonalCircles(address avatar) public returns (address inflationaryCircles) { + inflationaryCircles = LIFT_ERC20.ensureERC20(avatar, uint8(1)); + } + + /// @notice Returns backer's LBP status. + function isActiveLBP(address backer) external view returns (bool) { + address instance = circlesBackingOf[backer]; + uint256 unlockTimestamp = CirclesBacking(instance).balancerPoolTokensUnlockTimestamp(); + return unlockTimestamp > 0; + } + + // Internal functions + + // deploy instance + /** + * @notice Deploys a new CirclesBacking contract with CREATE2. + * @param backer Address which is backing circles. + * @return deployedAddress Address of the deployed contract. + */ + function deployCirclesBacking(address backer) internal returns (address deployedAddress) { + bytes32 salt_ = keccak256(abi.encodePacked(backer)); + + deployedAddress = address(new CirclesBacking{salt: salt_}()); + + if (deployedAddress == address(0) || deployedAddress.code.length == 0) { + revert CirclesBackingDeploymentFailed(backer); + } + + // link instance to backer + backerOf[deployedAddress] = backer; + // link backer to instance + circlesBackingOf[backer] = deployedAddress; + + emit CirclesBackingDeployed(backer, deployedAddress); + } + + // cowswap app data helper + /// @dev returns string as address value + function addressToString(address _addr) internal pure returns (string memory) { + bytes32 value = bytes32(uint256(uint160(_addr))); + bytes memory alphabet = "0123456789abcdef"; + + bytes memory str = new bytes(42); + str[0] = "0"; + str[1] = "x"; + + for (uint256 i = 0; i < 20; i++) { + str[2 + i * 2] = alphabet[uint8(value[i + 12] >> 4)]; + str[3 + i * 2] = alphabet[uint8(value[i + 12] & 0x0f)]; + } + + return string(str); + } + + // personal circles lbp name + function _name(address inflationaryCirlces) internal view returns (string memory) { + return string(abi.encodePacked(LBP_PREFIX, IERC20Metadata(inflationaryCirlces).name())); + } + + // personal circles lbp symbol + function _symbol(address inflationaryCirlces) internal view returns (string memory) { + return string(abi.encodePacked(LBP_PREFIX, IERC20Metadata(inflationaryCirlces).symbol())); + } + + // Callback + function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes calldata data) + external + nonReentrant + returns (bytes4) + { + if (msg.sender != address(HUB_V2)) revert OnlyHub(); + if (value != CRC_AMOUNT) revert NotExactlyRequiredCRCAmount(CRC_AMOUNT, value); + address avatar = address(uint160(id)); + if (!HUB_V2.isHuman(avatar)) revert OnlyHumanAvatarsAreSupported(); + if (operator != from || from != avatar) revert BackingInFavorDissalowed(); + // handling personal CRC + // get stable address + address stableCRC = getPersonalCircles(avatar); + + uint256 stableCirclesAmount = IERC20(stableCRC).balanceOf(address(this)); + // wrap erc1155 into stable ERC20 + HUB_V2.wrap(avatar, CRC_AMOUNT, uint8(1)); + stableCirclesAmount = IERC20(stableCRC).balanceOf(address(this)) - stableCirclesAmount; + + // decode backing asset + address backingAsset = abi.decode(data, (address)); + + startBacking(avatar, backingAsset, stableCRC, stableCirclesAmount); + return this.onERC1155Received.selector; + } +} diff --git a/src/factory/TestCirclesLBPFactory.sol b/src/factory/TestCirclesLBPFactory.sol deleted file mode 100644 index 92b8555..0000000 --- a/src/factory/TestCirclesLBPFactory.sol +++ /dev/null @@ -1,204 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity ^0.8.28; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import {IHub} from "src/interfaces/IHub.sol"; -import {IWXDAI} from "src/interfaces/IWXDAI.sol"; -import {ISXDAI} from "src/interfaces/ISXDAI.sol"; -import {IVault} from "src/interfaces/IVault.sol"; -import {ITestLBPMintPolicy} from "src/interfaces/ITestLBPMintPolicy.sol"; -import {ILiftERC20} from "src/interfaces/ILiftERC20.sol"; -import {INoProtocolFeeLiquidityBootstrappingPoolFactory} from "src/interfaces/ILBPFactory.sol"; -import {ILBP} from "src/interfaces/ILBP.sol"; - -/** - * @title Test version of Circles Liquidity Bootstraping Pool Factory. - * @notice Contract allows to create LBP and deposit BPT to related group TestLBPMintPolicy. - * Contract allows to exit pool by providing BPT back. - */ -contract TestCirclesLBPFactory { - /// Method can be called only by Liquidity Bootstraping Pool owner. - error OnlyLBPOwner(); - /// Method requires exact `requiredXDai` xDai amount, was provided: `providedXDai`. - error NotExactXDaiAmount(uint256 providedXDai, uint256 requiredXDai); - /// LBP was created previously for this `group` group, currently only 1 LBP per user can be created. - error OnlyOneLBPPerGroup(address group); - /// Mint Policy for this `group` group doesn't support CirclesLBPFactory. - error InvalidMintPolicy(address group); - /// User `avatar` doesn't have InflationaryCircles. - error InflationaryCirclesNotExists(address avatar); - /// Exit Liquidity Bootstraping Pool supports only two tokens pools. - error OnlyTwoTokenLBPSupported(); - - /// @notice Emitted when a LBP is created. - event LBPCreated(address indexed user, address indexed group, address indexed lbp); - - struct UserGroup { - address user; - address group; - } - - /// @dev BPT name and symbol prefix. - string internal constant LBP_PREFIX = "testLBP-"; - /// @notice Amount of xDai to use in LBP initial liquidity. - uint256 public constant XDAI_AMOUNT = 50 ether; - /// @notice Amount of InflationaryCircles to use in LBP initial liquidity. - uint256 public constant CRC_AMOUNT = 48 ether; - /// @dev LBP token weight 1%. - uint256 internal constant WEIGHT_1 = 0.01 ether; - /// @dev LBP token weight 99%. - uint256 internal constant WEIGHT_99 = 0.99 ether; - /// @dev LBP token weight 50%. - uint256 internal constant WEIGHT_50 = 0.5 ether; - - /// @notice Balancer v2 Vault. - address public constant VAULT = address(0xBA12222222228d8Ba445958a75a0704d566BF2C8); - /// @notice Balancer v2 LBPFactory. - INoProtocolFeeLiquidityBootstrappingPoolFactory public constant LBP_FACTORY = - INoProtocolFeeLiquidityBootstrappingPoolFactory(address(0x85a80afee867aDf27B50BdB7b76DA70f1E853062)); - /// @notice Circles Hub v2. - IHub public constant HUB_V2 = IHub(address(0xc12C1E50ABB450d6205Ea2C3Fa861b3B834d13e8)); - /// @notice Circles v2 LiftERC20 contract. - ILiftERC20 public constant LIFT_ERC20 = ILiftERC20(address(0x5F99a795dD2743C36D63511f0D4bc667e6d3cDB5)); - /// @notice Wrapped xDAI contract. - IWXDAI public constant WXDAI = IWXDAI(address(0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d)); - /// @notice Savings xDAI contract. - ISXDAI public constant SXDAI = ISXDAI(address(0xaf204776c7245bF4147c2612BF6e5972Ee483701)); - - mapping(address user => mapping(address group => address lbp)) public userGroupToLBP; - mapping(address lbp => UserGroup) public lbpToUserGroup; - - constructor() {} - - // LBP Factory logic - - /// @notice Creates LBP with underlying assets: `XDAI_AMOUNT` SxDAI and `CRC_AMOUNT` InflationaryCircles. - /// Balancer Pool Token receiver is Mint Policy related to `group` Group CRC. - /// Calls Group Mint Policy to trigger necessary actions related to user backing personal CRC. - /// @dev Required InflationaryCircles approval at least `CRC_AMOUNT` before call - /// swapFeePercentage bounds are: from 1e12 (0.0001%) to 1e17 (10%) - function createLBP(address group, uint256 swapFeePercentage, uint256 updateWeightDuration) external payable { - // check msg.value - if (msg.value != XDAI_AMOUNT) revert NotExactXDaiAmount(msg.value, XDAI_AMOUNT); - // for now only 1 lbp per group/user - if (userGroupToLBP[msg.sender][group] != address(0)) revert OnlyOneLBPPerGroup(group); - // check mint policy - address mintPolicy = HUB_V2.mintPolicies(group); - if (ITestLBPMintPolicy(mintPolicy).TEST_CIRCLES_LBP_FACTORY() != address(this)) revert InvalidMintPolicy(group); - - // check inflationaryCircles - address inflationaryCirlces = LIFT_ERC20.erc20Circles(uint8(1), msg.sender); - if (inflationaryCirlces == address(0)) revert InflationaryCirclesNotExists(msg.sender); - IERC20(inflationaryCirlces).transferFrom(msg.sender, address(this), CRC_AMOUNT); - // approve vault - IERC20(inflationaryCirlces).approve(address(VAULT), CRC_AMOUNT); - - // convert xDAI into SxDAI - WXDAI.deposit{value: msg.value}(); - WXDAI.approve(address(SXDAI), msg.value); - uint256 shares = SXDAI.deposit(msg.value, address(this)); - // approve vault - SXDAI.approve(address(VAULT), shares); - - // prepare inputs - IERC20[] memory tokens = new IERC20[](2); - bool tokenZero = inflationaryCirlces < address(SXDAI); - tokens[0] = tokenZero ? IERC20(address(inflationaryCirlces)) : IERC20(address(SXDAI)); - tokens[1] = tokenZero ? IERC20(address(SXDAI)) : IERC20(address(inflationaryCirlces)); - - uint256[] memory weights = new uint256[](2); - weights[0] = tokenZero ? WEIGHT_1 : WEIGHT_99; - weights[1] = tokenZero ? WEIGHT_99 : WEIGHT_1; - - // create LBP - address lbp = LBP_FACTORY.create( - _name(inflationaryCirlces), - _symbol(inflationaryCirlces), - tokens, - weights, - swapFeePercentage, - address(this), // lbp owner - true // enable swap on start - ); - // attach lbp to user/group - userGroupToLBP[msg.sender][group] = lbp; - // attach user/group to lbp - lbpToUserGroup[lbp] = UserGroup(msg.sender, group); - - emit LBPCreated(msg.sender, group, lbp); - - bytes32 poolId = ILBP(lbp).getPoolId(); - - uint256[] memory amountsIn = new uint256[](2); - amountsIn[0] = tokenZero ? CRC_AMOUNT : shares; - amountsIn[1] = tokenZero ? shares : CRC_AMOUNT; - - bytes memory userData = abi.encode(ILBP.JoinKind.INIT, amountsIn); - - // provide liquidity into lbp - IVault(VAULT).joinPool( - poolId, - address(this), // sender - mintPolicy, // recipient - IVault.JoinPoolRequest(tokens, amountsIn, userData, false) - ); - - // update weight gradually - ILBP(lbp).updateWeightsGradually(block.timestamp, block.timestamp + updateWeightDuration, _endWeights()); - - // call mint policy to account deposit - ITestLBPMintPolicy(mintPolicy).depositBPT(msg.sender, lbp); - } - - /// @notice General wrapper function over vault.exitPool, allows to extract - /// liquidity from pool by approving this Factory to spend Balancer Pool Tokens. - /// @dev Required Balancer Pool Token approval for bptAmount before call - function exitLBP(address lbp, uint256 bptAmount) external { - // transfer bpt tokens from msg.sender - IERC20(lbp).transferFrom(msg.sender, address(this), bptAmount); - - uint256[] memory minAmountsOut = new uint256[](2); - minAmountsOut[0] = uint256(0); - minAmountsOut[1] = uint256(0); - - bytes32 poolId = ILBP(lbp).getPoolId(); - - (IERC20[] memory poolTokens,,) = IVault(VAULT).getPoolTokens(poolId); - if (poolTokens.length != minAmountsOut.length) revert OnlyTwoTokenLBPSupported(); - - // exit pool - IVault(VAULT).exitPool( - poolId, - address(this), // sender - payable(msg.sender), // recipient - IVault.ExitPoolRequest( - poolTokens, minAmountsOut, abi.encode(ILBP.ExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT, bptAmount), false - ) - ); - } - - /** - * @dev Enable or disables swaps. - */ - function setSwapEnabled(address lbp, bool swapEnabled) external { - if (lbpToUserGroup[lbp].user != msg.sender) revert OnlyLBPOwner(); - ILBP(lbp).setSwapEnabled(swapEnabled); - } - - // Internal functions - - function _name(address inflationaryCirlces) internal view returns (string memory) { - return string(abi.encodePacked(LBP_PREFIX, IERC20Metadata(inflationaryCirlces).name())); - } - - function _symbol(address inflationaryCirlces) internal view returns (string memory) { - return string(abi.encodePacked(LBP_PREFIX, IERC20Metadata(inflationaryCirlces).symbol())); - } - - function _endWeights() internal pure returns (uint256[] memory endWeights) { - endWeights = new uint256[](2); - endWeights[0] = WEIGHT_50; - endWeights[1] = WEIGHT_50; - } -} diff --git a/src/interfaces/ICowswapSettlement.sol b/src/interfaces/ICowswapSettlement.sol new file mode 100644 index 0000000..d6192a8 --- /dev/null +++ b/src/interfaces/ICowswapSettlement.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.28; + +interface ICowswapSettlement { + function setPreSignature(bytes calldata orderUid, bool signed) external; + function filledAmount(bytes calldata orderUid) external view returns (uint256); +} diff --git a/src/interfaces/IFactory.sol b/src/interfaces/IFactory.sol new file mode 100644 index 0000000..bae720b --- /dev/null +++ b/src/interfaces/IFactory.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.28; + +import {IVault} from "src/interfaces/IVault.sol"; + +interface IFactory { + function getAppData(address _circlesBackingInstance) + external + pure + returns (string memory appDataString, bytes32 appDataHash); + function createLBP(address personalCRC, uint256 personalCRCAmount, address backingAsset, uint256 backingAssetAmount) + external + returns (address lbp, bytes32 poolId, IVault.JoinPoolRequest memory request, address vault); + function releaseTimestamp() external view returns (uint32); +} diff --git a/src/interfaces/IGetUid.sol b/src/interfaces/IGetUid.sol new file mode 100644 index 0000000..538805c --- /dev/null +++ b/src/interfaces/IGetUid.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.28; + +interface IGetUid { + function getUid( + address sellToken, + address buyToken, + address receiver, + uint256 sellAmount, + uint256 buyAmount, + uint32 validTo, + bytes32 appData, + uint256 feeAmount, + bool isSell, + bool partiallyFillable + ) external view returns (bytes32 hash, bytes memory encoded); +} diff --git a/src/interfaces/IHub.sol b/src/interfaces/IHub.sol index e948080..d673fe1 100644 --- a/src/interfaces/IHub.sol +++ b/src/interfaces/IHub.sol @@ -8,4 +8,5 @@ interface IHub is IHubV2 { function trust(address _trustReceiver, uint96 _expiry) external; function registerGroup(address _mint, string calldata _name, string calldata _symbol, bytes32 _metadataDigest) external; + function wrap(address _avatar, uint256 _amount, uint8 _type) external returns (address); } diff --git a/src/interfaces/ILiftERC20.sol b/src/interfaces/ILiftERC20.sol index fabb75a..1532629 100644 --- a/src/interfaces/ILiftERC20.sol +++ b/src/interfaces/ILiftERC20.sol @@ -2,5 +2,5 @@ pragma solidity ^0.8.28; interface ILiftERC20 { - function erc20Circles(uint8 erc20Type, address avatar) external view returns (address); + function ensureERC20(address _avatar, uint8 _circlesType) external returns (address); } diff --git a/src/interfaces/ITestLBPMintPolicy.sol b/src/interfaces/ITestLBPMintPolicy.sol deleted file mode 100644 index 3dbe6b1..0000000 --- a/src/interfaces/ITestLBPMintPolicy.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity ^0.8.28; - -interface ITestLBPMintPolicy { - function TEST_CIRCLES_LBP_FACTORY() external view returns (address); - function depositBPT(address user, address lbp) external; -} diff --git a/src/interfaces/ITestTrustModule.sol b/src/interfaces/ITestTrustModule.sol deleted file mode 100644 index fdcab36..0000000 --- a/src/interfaces/ITestTrustModule.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity ^0.8.28; - -interface ITestTrustModule { - function setSafe(address safe) external; - function approveMintPolicy(address mintPolicy) external; - function trust(address avatar) external; - function untrust(address avatar) external; -} diff --git a/src/module/TestTrustModule.sol b/src/module/TestTrustModule.sol deleted file mode 100644 index 09e7b24..0000000 --- a/src/module/TestTrustModule.sol +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity ^0.8.28; - -import {IHub} from "src/interfaces/IHub.sol"; -import {Safe} from "safe-smart-account/contracts/Safe.sol"; -import {Enum} from "safe-smart-account/contracts/common/Enum.sol"; - -/** - * @title Test version of Safe Trust Module. - * @notice Contract on Mint Policy request calls Hub from Safe to trust/untrust avatar. - */ -contract TestTrustModule { - /// Safe `safe` has disabled this module. - error ModuleDisabledBySafe(address safe); - /// Attempt to execute trust/untrust failed during executionFromModule call. - error ExecutionFromModuleFailed(); - /// Mint policy `mintPolicy` is missing approval from Safe `safe`. - error MintPolicyNotApproved(address mintPolicy, address safe); - - /// @notice Emitted when safe trusts avatar by mint policy request. - event Trust(address indexed avatar, address indexed safe, address indexed mintPolicy); - /// @notice Emitted when safe untrusts avatar by mint policy request. - event Untrust(address indexed avatar, address indexed safe, address indexed mintPolicy); - - /// @dev Maximum value for Hub trust expiration. - uint96 internal constant INDEFINITE_FUTURE = type(uint96).max; - - /// @notice Circles Hub v2. - address public constant HUB_V2 = address(0xc12C1E50ABB450d6205Ea2C3Fa861b3B834d13e8); - - mapping(address mintPolicy => Safe safe) public mintPolicyToSafe; - mapping(address safe => address mintPolicy) public safeToMintPolicy; - - constructor() {} - - // Register logic - - /// @notice Allows Mint policy to set its Safe. - function setSafe(address safe) external { - mintPolicyToSafe[msg.sender] = Safe(payable(safe)); - } - - /// @notice Allows Safe to approve Mint policy. - function approveMintPolicy(address mintPolicy) external { - safeToMintPolicy[msg.sender] = mintPolicy; - } - - // Trust logic - - /// @notice Allows mint policy to request Safe call to trust avatar. - function trust(address avatar) external { - Safe safe = _validateSafe(); - _executeTrustRequest(safe, avatar, INDEFINITE_FUTURE); - emit Trust(avatar, address(safe), msg.sender); - } - - /// @notice Allows mint policy to request Safe call to untrust avatar. - function untrust(address avatar) external { - Safe safe = _validateSafe(); - _executeTrustRequest(safe, avatar, uint96(block.timestamp)); - emit Untrust(avatar, address(safe), msg.sender); - } - - // Internal functions - - function _validateSafe() internal view returns (Safe) { - Safe safe = mintPolicyToSafe[msg.sender]; - if (safeToMintPolicy[address(safe)] != msg.sender) revert MintPolicyNotApproved(msg.sender, address(safe)); - if (!safe.isModuleEnabled(address(this))) revert ModuleDisabledBySafe(address(safe)); - return safe; - } - - function _executeTrustRequest(Safe safe, address avatar, uint96 expiry) internal { - bytes memory data = abi.encodeWithSelector(IHub.trust.selector, avatar, expiry); - bool success = safe.execTransactionFromModule(HUB_V2, 0, data, Enum.Operation.Call); - if (!success) revert ExecutionFromModuleFailed(); - } -} diff --git a/src/policy/GroupDemurrage.sol b/src/policy/GroupDemurrage.sol deleted file mode 100644 index 80d39e0..0000000 --- a/src/policy/GroupDemurrage.sol +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity ^0.8.28; - -import {ABDKMath64x64 as Math64x64} from "lib/circles-contracts-v2/lib/abdk-libraries-solidity/ABDKMath64x64.sol"; - -contract GroupDemurrage { - /// @dev Discounted balance with a last updated timestamp. - struct DiscountedBalance { - uint192 balance; - uint64 lastUpdatedDay; - } - - // Constants - - /** - * @notice Demurrage window reduces the resolution for calculating - * the demurrage of balances from once per second (block.timestamp) - * to once per day. - */ - uint256 private constant DEMURRAGE_WINDOW = 1 days; - - /** - * @dev Maximum value that can be stored or transferred - */ - uint256 internal constant MAX_VALUE = type(uint192).max; - - /** - * @dev Reduction factor GAMMA for applying demurrage to balances - * demurrage_balance(d) = GAMMA^d * inflationary_balance - * where 'd' is expressed in days (DEMURRAGE_WINDOW) since demurrage_day_zero, - * and GAMMA < 1. - * GAMMA_64x64 stores the numerator for the signed 128bit 64.64 - * fixed decimal point expression: - * GAMMA = GAMMA_64x64 / 2**64. - * To obtain GAMMA for a daily accounting of 7% p.a. demurrage - * => GAMMA = (0.93)^(1/365.25) - * = 0.99980133200859895743... - * and expressed in 64.64 fixed point representation: - * => GAMMA_64x64 = 18443079296116538654 - * For more details, see ./specifications/TCIP009-demurrage.md - */ - int128 internal constant GAMMA_64x64 = int128(18443079296116538654); - - /** - * @notice Inflation day zero stores the start of the global inflation curve - * As Circles Hub v1 was deployed on Thursday 15th October 2020 at 6:25:30 pm UTC, - * or 1602786330 unix time, in production this value MUST be set to 1602720000 unix time, - * or midnight prior of the same day of deployment, marking the start of the first day - * where there was no inflation on one CRC per hour. - */ - uint256 internal constant inflationDayZero = 1602720000; - - // Internal functions - - /** - * @notice Calculate the day since inflation_day_zero for a given timestamp. - * @param _timestamp Timestamp for which to calculate the day since inflation_day_zero. - */ - function day(uint256 _timestamp) internal pure returns (uint64) { - // calculate which day the timestamp is in, rounding down - // note: max uint64 is 2^64 - 1, so we can safely cast the result - return uint64((_timestamp - inflationDayZero) / DEMURRAGE_WINDOW); - } - - /** - * @dev Calculates the discounted balance given a number of days to discount - * @param _balance balance to calculate the discounted balance of - * @param _daysDifference days of difference between the last updated day and the day of interest - */ - function _calculateDiscountedBalance(uint256 _balance, uint256 _daysDifference) internal pure returns (uint256) { - if (_daysDifference == 0) { - return _balance; - } - int128 r = _calculateDemurrageFactor(_daysDifference); - return Math64x64.mulu(r, _balance); - } - - function _calculateDemurrageFactor(uint256 _dayDifference) internal pure returns (int128) { - // calculate the value - return Math64x64.pow(GAMMA_64x64, _dayDifference); - } -} diff --git a/src/policy/TestLBPMintPolicy.sol b/src/policy/TestLBPMintPolicy.sol deleted file mode 100644 index f706fb8..0000000 --- a/src/policy/TestLBPMintPolicy.sol +++ /dev/null @@ -1,282 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity ^0.8.28; - -import {MintPolicy, IMintPolicy, BaseMintPolicyDefinitions} from "circles-contracts-v2/groups/BaseMintPolicy.sol"; -import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; -import {GroupDemurrage} from "src/policy/GroupDemurrage.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {ITestTrustModule} from "src/interfaces/ITestTrustModule.sol"; - -/** - * @title Test version of Liquidity Bootstraping Pool Mint Policy. - * @notice Contract extends MintPolicy with LBP, allowing mints only to - * LBPFactory users and accounts their mints. BPT withdrawal is - * allowed only on zeroed mints. - */ -contract TestLBPMintPolicy is Initializable, GroupDemurrage, MintPolicy { - /// Method can be called only by Hub contract. - error OnlyHubV2(); - /// Method can be called only by StandardTreasury contract. - error OnlyStandardTreasury(); - /// Method can be called only by CirclesLBPFactory contract. - error OnlyCirclesLBPFactory(); - /// Requested group avatar by Hub doesn't match the group avatar this policy is attached to. - error GroupAvatarMismatch(); - /// This `lbp` LBP is already set for this `user` user. - error LBPAlreadySet(address user, address lbp); - /// Before withdraw is required to redeem or burn minted group circle amount: `mintedAmount`. - error MintedAmountNotZero(uint256 mintedAmount); - - /// @notice Emitted when a Balancer Pool Tokens are deposited to the policy. - event BPTDeposit(address indexed user, address indexed lbp, uint256 indexed bptAmount); - /// @notice Emitted when a Balancer Pool Tokens are withdrawn from the policy. - event BPTWithdrawal(address indexed user, address indexed lbp, uint256 indexed bptAmount); - - struct LBP { - address lbp; - uint256 bptAmount; - } - - /// @custom:storage-location erc7201:circles-test.storage.TestLBPMintPolicy - struct TestLBPMintPolicyStorage { - address groupAvatar; - mapping(address user => LBP) lbps; - mapping(address minter => DiscountedBalance) mintedAmounts; - } - - // keccak256(abi.encode(uint256(keccak256("circles-test.storage.TestLBPMintPolicy")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant TestLBPMintPolicyStorageLocation = - 0xca29e300055a7452862813c656216e9b6f0fc137dc564e51d7176af282c11600; - - /// @notice Circles Hub v2. - address public constant HUB_V2 = address(0xc12C1E50ABB450d6205Ea2C3Fa861b3B834d13e8); - /// @notice Circles v2 StandardTreasury. - address public constant STANDARD_TREASURY = address(0x08F90aB73A515308f03A718257ff9887ED330C6e); - /// @notice Test version of CirclesLBPFactory. - address public constant TEST_CIRCLES_LBP_FACTORY = address(0x97030b525248cAc78aabcc33D37139BfB5a34750); - /// @notice Test version of TrustModule. - ITestTrustModule public constant TEST_TRUST_MODULE = - ITestTrustModule(address(0x56652E53649F20C6a360Ea5F25379F9987cECE82)); - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - function initialize() external initializer { - TestLBPMintPolicyStorage storage $ = _getTestLBPMintPolicyStorage(); - $.groupAvatar = msg.sender; - TEST_TRUST_MODULE.setSafe(msg.sender); - } - - // Hub Mint Policy logic - - /** - * @notice Before mint checks and allows to mint only if user has lbp, accounts minted amount - */ - function beforeMintPolicy( - address minter, - address group, - uint256[] calldata, /*_collateral*/ - uint256[] calldata amounts, - bytes calldata /*_data*/ - ) external virtual override returns (bool) { - _onlyHubV2(); - _checkGroupAvatar(group); - // mint is allowed only for lbp factory users - if (_getBPTAmount(minter) == 0) return false; - // account minted amount - uint256 totalAmount; - for (uint256 i; i < amounts.length;) { - totalAmount += amounts[i]; - unchecked { - ++i; - } - } - _accountMintedAmount(minter, totalAmount, true); - return true; - } - - /** - * @notice Simple burn policy that always returns true and accounts burn for LBP user - */ - function beforeBurnPolicy(address burner, address group, uint256 amount, bytes calldata) - external - virtual - override - returns (bool) - { - _onlyHubV2(); - _checkGroupAvatar(group); - if (_getBPTAmount(burner) > 0) { - _accountMintedAmount(burner, amount, false); - } - return true; - } - - /** - * @notice Simple redeem policy that returns the redemption ids and values as requested in the data - * Accounts redeem in minted amount for LBP user. - * @param _data Optional data bytes passed to redeem policy - */ - function beforeRedeemPolicy( - address, /* operator */ - address redeemer, - address group, - uint256 value, - bytes calldata _data - ) - external - virtual - override - returns ( - uint256[] memory _ids, - uint256[] memory _values, - uint256[] memory _burnIds, - uint256[] memory _burnValues - ) - { - if (msg.sender != STANDARD_TREASURY) revert OnlyStandardTreasury(); - _checkGroupAvatar(group); - if (_getBPTAmount(redeemer) > 0) { - _accountMintedAmount(redeemer, value, false); - } - - // simplest policy is to return the collateral as the caller requests it in data - BaseMintPolicyDefinitions.BaseRedemptionPolicy memory redemption = - abi.decode(_data, (BaseMintPolicyDefinitions.BaseRedemptionPolicy)); - - // and no collateral gets burnt upon redemption - _burnIds = new uint256[](0); - _burnValues = new uint256[](0); - - // standard treasury checks whether the total sums add up to the amount of group Circles redeemed - // so we can simply decode and pass the request back to treasury. - // The redemption will fail if it does not contain (sufficient of) these Circles - return (redemption.redemptionIds, redemption.redemptionValues, _burnIds, _burnValues); - } - - // LBP Factory logic - - /** - * @notice Method should be called by CirclesLBPFactory after LBP onJoinPool with BPT recipient address(this). - * Accounts BPT deposit and allows user to mint group token. - * Asks group to trust user avatar as a group collateral. - */ - function depositBPT(address user, address lbp) external { - if (msg.sender != TEST_CIRCLES_LBP_FACTORY) revert OnlyCirclesLBPFactory(); - if (_getBPTAmount(user) > 0) revert LBPAlreadySet(user, lbp); - // bpt amount should be transfered before this call by factory - uint256 bptAmount = IERC20(lbp).balanceOf(address(this)); - _setLBP(user, lbp, bptAmount); - emit BPTDeposit(user, lbp, bptAmount); - // safe.module try groupAvatar trust user - try TEST_TRUST_MODULE.trust(user) {} catch {} - } - - /** - * @notice Method allows LBP user to withdraw Balancer Pool Tokens related to LBP only - * if user current minted group CRC amount is zero. - * Accounts BPT withdrawal and disallows user to mint group token. - * Asks group to untrust user avatar as a group collateral. - */ - function withdrawBPT() external { - address user = msg.sender; - uint256 mintedAmountOnToday; - (mintedAmountOnToday,) = _getMintedAmountOnToday(user); - if (mintedAmountOnToday != 0) revert MintedAmountNotZero(mintedAmountOnToday); - - address lbp = _getLBPAddress(user); - uint256 bptAmount = _getBPTAmount(user); - _setLBP(user, lbp, 0); - IERC20(lbp).transfer(user, bptAmount); - emit BPTWithdrawal(user, lbp, bptAmount); - // safe.module try groupAvatar untrust user - try TEST_TRUST_MODULE.untrust(user) {} catch {} - } - - // View functions - - function getGroupAvatar() external view returns (address) { - return _getGroupAvatar(); - } - - function getBPTAmount(address user) external view returns (uint256) { - return _getBPTAmount(user); - } - - function getLBPAddress(address user) external view returns (address) { - return _getLBPAddress(user); - } - - function getMintedAmount(address user) external view returns (uint256 mintedAmount) { - (mintedAmount,) = _getMintedAmountOnToday(user); - } - - // Internal functions - - function _onlyHubV2() internal view { - if (msg.sender != HUB_V2) revert OnlyHubV2(); - } - - function _checkGroupAvatar(address group) internal view { - if (group != _getGroupAvatar()) revert GroupAvatarMismatch(); - } - - function _accountMintedAmount(address minter, uint256 amount, bool add) internal { - (uint256 mintedAmountOnToday, uint64 today) = _getMintedAmountOnToday(minter); - uint256 updatedBalance; - if (add) { - updatedBalance = mintedAmountOnToday + amount; - require(updatedBalance <= MAX_VALUE); - } else if (amount < mintedAmountOnToday) { - updatedBalance = mintedAmountOnToday - amount; - } - _setMintedAmount(minter, uint192(updatedBalance), today); - } - - function _getMintedAmountOnToday(address user) internal view returns (uint256 mintedAmountOnToday, uint64 today) { - DiscountedBalance memory discountedBalance = _getMintedAmount(user); - today = day(block.timestamp); - mintedAmountOnToday = - _calculateDiscountedBalance(discountedBalance.balance, today - discountedBalance.lastUpdatedDay); - } - - // Private functions - - function _getTestLBPMintPolicyStorage() private pure returns (TestLBPMintPolicyStorage storage $) { - assembly { - $.slot := TestLBPMintPolicyStorageLocation - } - } - - function _getGroupAvatar() private view returns (address) { - TestLBPMintPolicyStorage storage $ = _getTestLBPMintPolicyStorage(); - return $.groupAvatar; - } - - function _getBPTAmount(address user) private view returns (uint256) { - TestLBPMintPolicyStorage storage $ = _getTestLBPMintPolicyStorage(); - return $.lbps[user].bptAmount; - } - - function _getLBPAddress(address user) private view returns (address) { - TestLBPMintPolicyStorage storage $ = _getTestLBPMintPolicyStorage(); - return $.lbps[user].lbp; - } - - function _setLBP(address user, address lbp_, uint256 bptAmount_) private { - TestLBPMintPolicyStorage storage $ = _getTestLBPMintPolicyStorage(); - $.lbps[user] = LBP({lbp: lbp_, bptAmount: bptAmount_}); - } - - function _getMintedAmount(address minter) private view returns (DiscountedBalance memory) { - TestLBPMintPolicyStorage storage $ = _getTestLBPMintPolicyStorage(); - return $.mintedAmounts[minter]; - } - - function _setMintedAmount(address minter, uint192 amount, uint64 day) private { - TestLBPMintPolicyStorage storage $ = _getTestLBPMintPolicyStorage(); - $.mintedAmounts[minter] = DiscountedBalance({balance: amount, lastUpdatedDay: day}); - } -} diff --git a/src/prototype/Factory.sol b/src/prototype/Factory.sol new file mode 100644 index 0000000..300371a --- /dev/null +++ b/src/prototype/Factory.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0 <0.9.0; + +import "./OrderCreator.sol"; + +/** + * @title Factory for deterministic deployment of OrderCreator contracts + * @dev Uses CREATE2 for predictable contract address deployment independent of deployer. + */ +contract OrderCreatorFactory { + event OrderCreatorDeployed(address indexed deployedAddress, address indexed receiver); + + /** + * @notice Deploys a new OrderCreator contract with CREATE2. + * @param receiver Address to receive GNO. + * @return deployedAddress Address of the deployed contract. + */ + function deployOrderCreator(address receiver) external returns (address deployedAddress) { + require(receiver != address(0), "Receiver address cannot be zero"); + + bytes32 salt = keccak256(abi.encodePacked(receiver)); + bytes memory bytecode = abi.encodePacked(type(OrderCreator).creationCode, abi.encode(receiver)); + + assembly { + deployedAddress := create2(0, add(bytecode, 0x20), mload(bytecode), salt) + if iszero(extcodesize(deployedAddress)) { revert(0, 0) } + } + + emit OrderCreatorDeployed(deployedAddress, receiver); + } + + /** + * @notice Computes the deterministic address for an OrderCreator contract. + * @param receiver Address to receive GNO. + * @return predictedAddress Predicted address of the deployed contract. + */ + function computeAddress(address receiver) external view returns (address predictedAddress) { + require(receiver != address(0), "Receiver address cannot be zero"); + bytes32 salt = keccak256(abi.encodePacked(receiver)); + bytes memory bytecode = abi.encodePacked(type(OrderCreator).creationCode, abi.encode(receiver)); + predictedAddress = address( + uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(bytecode))))) + ); + } +} diff --git a/src/prototype/OrderCreator.sol b/src/prototype/OrderCreator.sol new file mode 100644 index 0000000..951eef8 --- /dev/null +++ b/src/prototype/OrderCreator.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +interface IERC20 { + function approve(address spender, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function transfer(address recipient, uint256 amount) external returns (bool); +} + +interface IGetUid { + function getUid( + address sellToken, + address buyToken, + address receiver, + uint256 sellAmount, + uint256 buyAmount, + uint32 validTo, + bytes32 appData, + uint256 feeAmount, + bool isSell, + bool partiallyFillable + ) external view returns (bytes32 hash, bytes memory encoded); +} + +interface ICowswapSettlement { + function setPreSignature(bytes calldata orderUid, bool signed) external; + function filledAmount(bytes calldata orderUid) external view returns (uint256); +} + +contract OrderCreator { + address public constant WXDAI = 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d; + address public constant GNO = 0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb; + address public constant GET_UID_CONTRACT = 0xCA51403B524dF7dA6f9D6BFc64895AD833b5d711; + address public constant COWSWAP_SETTLEMENT_CONTRACT = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; + address public constant RECEIVER = 0x6BF173798733623cc6c221eD52c010472247d861; + address public constant VAULT_RELAY = 0xC92E8bdf79f0507f65a392b0ab4667716BFE0110; + + uint256 public constant WXDAI_DECIMALS = 1e18; + uint256 public constant TRADE_AMOUNT = 100000000000000000; // 0.1 wxDAI in 18 decimals + uint32 public constant VALID_TO = uint32(1894006860); + + bytes public storedOrderUid; + + event OrderCreated(bytes32 orderHash); + event GnoTransferred(uint256 amount, address receiver); + + string public constant preAppData = + '{"version":"1.1.0","appCode":"Zeal powered by Qantura","metadata":{"hooks":{"version":"0.1.0","post":[{"target":"'; + string public constant postAppData = '","callData":"0xbb5ae136","gasLimit":"200000"}]}}}'; // Updated calldata for checkOrderFilledAndTransfer + + function addressToString(address _addr) internal pure returns (string memory) { + bytes32 value = bytes32(uint256(uint160(_addr))); + bytes memory alphabet = "0123456789abcdef"; + + bytes memory str = new bytes(42); + str[0] = "0"; + str[1] = "x"; + + for (uint256 i = 0; i < 20; i++) { + str[2 + i * 2] = alphabet[uint8(value[i + 12] >> 4)]; + str[3 + i * 2] = alphabet[uint8(value[i + 12] & 0x0f)]; + } + + return string(str); + } + + function getAppData(address _newAccount) public pure returns (bytes32) { + string memory _newAccountStr = addressToString(_newAccount); + string memory _appDataStr = string.concat(preAppData, _newAccountStr, postAppData); + return keccak256(bytes(_appDataStr)); + } + + function getAppDataString(address _newAccount) public pure returns (string memory) { + string memory _newAccountStr = addressToString(_newAccount); + return string.concat(preAppData, _newAccountStr, postAppData); + } + + function createOrder() external { + // Approve wxDAI to Vault Relay contract + IERC20(WXDAI).approve(VAULT_RELAY, TRADE_AMOUNT); + + // Generate appData dynamically + bytes32 appData = getAppData(address(this)); + + // Generate order UID using the "getUid" contract + IGetUid getUidContract = IGetUid(GET_UID_CONTRACT); + + (bytes32 orderDigest,) = getUidContract.getUid( + WXDAI, + GNO, + address(this), // Use contract address as the receiver + TRADE_AMOUNT, + 1, // Determined by off-chain logic or Cowswap solvers + VALID_TO, // ValidTo timestamp + appData, + 0, // FeeAmount + true, // IsSell + false // PartiallyFillable + ); + + // Construct the order UID + bytes memory orderUid = abi.encodePacked(orderDigest, address(this), uint32(VALID_TO)); + + // Store the order UID + storedOrderUid = orderUid; + + // Place the order using "setPreSignature" + ICowswapSettlement cowswapSettlement = ICowswapSettlement(COWSWAP_SETTLEMENT_CONTRACT); + cowswapSettlement.setPreSignature(orderUid, true); + + // Emit event with the order UID + emit OrderCreated(orderDigest); + } + + function checkOrderFilledAndTransfer() public { + // Check if the order has been filled on the CowSwap settlement contract + ICowswapSettlement cowswapSettlement = ICowswapSettlement(COWSWAP_SETTLEMENT_CONTRACT); + uint256 filledAmount = cowswapSettlement.filledAmount(storedOrderUid); + + require(filledAmount > 0, "Order not filled yet"); + + // Check GNO balance of the contract + uint256 gnoBalance = IERC20(GNO).balanceOf(address(this)); + require(gnoBalance > 0, "No GNO balance to transfer"); + + // Transfer GNO to the receiver + bool success = IERC20(GNO).transfer(RECEIVER, gnoBalance); + require(success, "GNO transfer failed"); + + // Emit event for the transfer + emit GnoTransferred(gnoBalance, RECEIVER); + } +} diff --git a/test/CirclesBackingFactory.t.sol b/test/CirclesBackingFactory.t.sol new file mode 100644 index 0000000..da32789 --- /dev/null +++ b/test/CirclesBackingFactory.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.28; + +import {Test, console} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {CirclesBacking} from "src/CirclesBacking.sol"; +import {CirclesBackingFactory} from "src/factory/CirclesBackingFactory.sol"; +import {IVault} from "src/interfaces/IVault.sol"; +import {ILBP} from "src/interfaces/ILBP.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IHub} from "src/interfaces/IHub.sol"; + +contract CirclesBackingFactoryTest is Test { + address public constant COWSWAP_SETTLEMENT = address(0x9008D19f58AAbD9eD0D60971565AA8510560ab41); + IHub public constant HUB_V2 = IHub(address(0xc12C1E50ABB450d6205Ea2C3Fa861b3B834d13e8)); + CirclesBackingFactory public factory; + address factoryAdmin = address(0x4583759874359754305480345); + address testAccount = address(0x0865d14a4B688F24Bc8C282045A4A3cb9a26FbC2); + address WETH = address(0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1); + address personalCRC; + address backingAsset; + address VAULT; + address USDC; + uint256 usdcStartAmount = 100e6; + uint256 CRC_AMOUNT; + + uint256 blockNumber = 37997675; + uint256 gnosis; + + bytes public uid; + + function setUp() public { + gnosis = vm.createFork(vm.envString("GNOSIS_RPC"), blockNumber); + vm.selectFork(gnosis); + factory = new CirclesBackingFactory(factoryAdmin, uint256(100)); + VAULT = factory.VAULT(); + USDC = factory.USDC(); + CRC_AMOUNT = factory.CRC_AMOUNT(); + } + + function test_BackingFlow() public { + address predictedInstance = factory.computeAddress(testAccount); + + // first fill test account with 100 USDC + deal(USDC, testAccount, usdcStartAmount); + + // next approve factory to spend usdc + vm.prank(testAccount); + IERC20(USDC).approve(address(factory), usdcStartAmount); + + // next transfer 48CRC to factory with WETH encoded as backing asset + bytes memory data = abi.encode(WETH); + vm.prank(testAccount); + HUB_V2.safeTransferFrom(testAccount, address(factory), uint256(uint160(testAccount)), CRC_AMOUNT, data); + + // next simulate actions done by cowswap solvers + // 1. set some instance balance of backing asset + deal(WETH, predictedInstance, 0.03 ether); + // 2. set settlement contract state filledAmount at uid key with 0.03 ether + uid = CirclesBacking(predictedInstance).storedOrderUid(); + bytes32 slot = keccak256(abi.encodePacked(uid, uint256(2))); + vm.store(COWSWAP_SETTLEMENT, slot, bytes32(uint256(0.03 ether))); + + // next call createLBP instead of cowswap solver + CirclesBacking(predictedInstance).createLBP(); + } +} diff --git a/test/CreateTestProxyLBPMintPolicy.t.sol b/test/CreateTestProxyLBPMintPolicy.t.sol index 39d385a..679a855 100644 --- a/test/CreateTestProxyLBPMintPolicy.t.sol +++ b/test/CreateTestProxyLBPMintPolicy.t.sol @@ -4,11 +4,15 @@ pragma solidity ^0.8.28; import {Test, console} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {CreateTestProxyLBPMintPolicy} from "src/proxy/CreateTestProxyLBPMintPolicy.sol"; -import {TestLBPMintPolicy} from "src/policy/TestLBPMintPolicy.sol"; -import {TestTrustModule} from "src/module/TestTrustModule.sol"; contract MockImplementation { uint256 constant a = 1; + uint256 public b; + + function initialize() external { + require(b == 0); + b = 1; + } } contract MockSafe { @@ -23,12 +27,9 @@ event AdminChanged(address previousAdmin, address newAdmin); contract CreateTestProxyLBPMintPolicyTest is Test { CreateTestProxyLBPMintPolicy public proxyDeployer; address public mockImplementation = address(new MockImplementation()); - address public implementation = address(new TestLBPMintPolicy()); - address public trustModule = address(0x56652E53649F20C6a360Ea5F25379F9987cECE82); function setUp() public { - proxyDeployer = new CreateTestProxyLBPMintPolicy(implementation); - deployCodeTo("TestTrustModule.sol", trustModule); + proxyDeployer = new CreateTestProxyLBPMintPolicy(mockImplementation); } function testFuzz_OnlyDelegateCall(address any) public { @@ -53,8 +54,8 @@ contract CreateTestProxyLBPMintPolicyTest is Test { safe.delegateTx(address(proxyDeployer), data); Vm.Log[] memory entries = vm.getRecordedLogs(); - address proxy = address(uint160(uint256(entries[3].topics[1]))); - assertEq(TestLBPMintPolicy(proxy).getGroupAvatar(), address(safe)); + address proxy = address(uint160(uint256(entries[2].topics[1]))); + console.log(proxy); } function _emitAdminChanged(address newAdmin) internal { diff --git a/test/DepositFlow.t.sol b/test/DepositFlow.t.sol deleted file mode 100644 index e25e2e2..0000000 --- a/test/DepositFlow.t.sol +++ /dev/null @@ -1,220 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity ^0.8.28; - -import {Test, console} from "forge-std/Test.sol"; -import {Vm} from "forge-std/Vm.sol"; -import {TestTrustModule} from "src/module/TestTrustModule.sol"; -import {TestCirclesLBPFactory} from "src/factory/TestCirclesLBPFactory.sol"; -import {TestLBPMintPolicy} from "src/policy/TestLBPMintPolicy.sol"; -import {CreateTestProxyLBPMintPolicy} from "src/proxy/CreateTestProxyLBPMintPolicy.sol"; -import {Safe} from "safe-smart-account/contracts/Safe.sol"; -import {Enum} from "safe-smart-account/contracts/common/Enum.sol"; -import {ModuleManager} from "safe-smart-account/contracts/base/ModuleManager.sol"; -import {Hub} from "circles-contracts-v2/hub/Hub.sol"; -import {CirclesType} from "circles-contracts-v2/lift/IERC20Lift.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {TypeDefinitions} from "circles-contracts-v2/hub/TypeDefinitions.sol"; -import {BaseMintPolicyDefinitions} from "circles-contracts-v2/groups/Definitions.sol"; -import {RedeemHelper} from "src/helpers/RedeemHelper.sol"; - -contract DepositFlowTest is Test { - uint256 blockNumber = 37_456_676; - uint256 gnosis; - // constants - bytes32 internal constant METADATATYPE_GROUPREDEEM = keccak256("CIRCLESv2:RESERVED_DATA:CirclesGroupRedeem"); - // deployment - address public constant STANDARD_TREASURY = address(0x08F90aB73A515308f03A718257ff9887ED330C6e); - Hub public hub = Hub(address(0xc12C1E50ABB450d6205Ea2C3Fa861b3B834d13e8)); - TestTrustModule public trustModule = TestTrustModule(address(0x56652E53649F20C6a360Ea5F25379F9987cECE82)); - TestCirclesLBPFactory public circlesLBPFactory = - TestCirclesLBPFactory(address(0x97030b525248cAc78aabcc33D37139BfB5a34750)); - address public implementationLBPMintPolicy = address(0xCb10eC7A4D9D764b1DcfcB9c2EBa675B1e756C96); - CreateTestProxyLBPMintPolicy public proxyDeployer = - CreateTestProxyLBPMintPolicy(address(0x777f78921890Df5Db755e77CbA84CBAdA5DB56D2)); - // test values - Safe testGroupSafe = Safe(payable(address(0x8bD2e75661Af98037b1Fc9fa0f9435baAa6Dd5ac))); - address proxy; - address testAccount = address(0x2A6878e8e34647533C5AA46012008ABfdF496988); - // helper - RedeemHelper public redeemHelper; - - function setUp() public { - gnosis = vm.createFork(vm.envString("GNOSIS_RPC"), blockNumber); - vm.selectFork(gnosis); - - // 1. first setup step for a TestGroup is to deploy proxy by Safe (requires crafting signatures): - // delegatecall from Safe to proxyDeployer - bytes memory data = abi.encodeWithSelector(CreateTestProxyLBPMintPolicy.createTestProxyMintPolicy.selector); - vm.recordLogs(); - _executeSafeTx(testGroupSafe, address(proxyDeployer), data, Enum.Operation.DelegateCall); - Vm.Log[] memory entries = vm.getRecordedLogs(); - proxy = address(uint160(uint256(entries[5].topics[1]))); - - // 2. second setup step for a TestGroup is to approve mint policy in TrustModule and enable TrustModule - - // call approve mint policy (proxy) - data = abi.encodeWithSelector(TestTrustModule.approveMintPolicy.selector, proxy); - _executeSafeTx(testGroupSafe, address(trustModule), data, Enum.Operation.Call); - - // call enableModule() on Safe to enable TrustModule - data = abi.encodeWithSelector(ModuleManager.enableModule.selector, address(trustModule)); - _executeSafeTx(testGroupSafe, address(testGroupSafe), data, Enum.Operation.Call); - - // 3. third setup step for a TestGroup is to registerGroup in Hub with proxy as a mint policy - data = abi.encodeWithSelector(Hub.registerGroup.selector, proxy, "testGroup", "TG", bytes32(0)); - _executeSafeTx(testGroupSafe, address(hub), data, Enum.Operation.Call); - - redeemHelper = new RedeemHelper(); - } - - function testDepositFlow() public { - _createLBPGroupMint(); - } - - function testWithdrawSameTimestampMint() public { - _createLBPGroupMint(); - // 1. redeem collateral group - uint256[] memory redemptionIds = new uint256[](1); - redemptionIds[0] = uint256(uint160(address(testAccount))); - uint256[] memory redemptionValues = new uint256[](1); - redemptionValues[0] = 5 ether; - - bytes memory data = redeemHelper.convertRedemptionToBytes(redemptionIds, redemptionValues); - vm.prank(testAccount); - hub.safeTransferFrom(testAccount, STANDARD_TREASURY, uint256(uint160(address(testGroupSafe))), 5 ether, data); - - address lbp = TestLBPMintPolicy(proxy).getLBPAddress(testAccount); - // 2. burn group token - vm.prank(testAccount); - hub.burn(uint256(uint160(address(testGroupSafe))), 5 ether, ""); - - vm.prank(testAccount); - TestLBPMintPolicy(proxy).withdrawBPT(); - - // 3. withdraw liquidity - uint256 balance = IERC20(lbp).balanceOf(testAccount); - vm.prank(testAccount); - IERC20(lbp).approve(address(circlesLBPFactory), balance); - vm.prank(testAccount); - circlesLBPFactory.exitLBP(lbp, balance); - } - - function testWithdrawAfterDurationMint() public { - _createLBPGroupMint(); - uint256 amount = 9980150952490564255; // 9996027034861687221 - uint256 duration = 10 days; // 2 days - _withdrawAfter(duration, amount); - } - - // Internal helpers - - function _withdrawAfter(uint256 duration, uint256 amount) internal { - vm.warp(block.timestamp + duration); - uint256 amountToRedeem = amount / 2; - uint256 amountToBurn = amount - amountToRedeem; - // 1. redeem collateral group - uint256[] memory redemptionIds = new uint256[](1); - redemptionIds[0] = uint256(uint160(address(testAccount))); - uint256[] memory redemptionValues = new uint256[](1); - redemptionValues[0] = amountToRedeem; - - bytes memory userData = - abi.encode(BaseMintPolicyDefinitions.BaseRedemptionPolicy(redemptionIds, redemptionValues)); - - bytes memory data = abi.encode(TypeDefinitions.Metadata(METADATATYPE_GROUPREDEEM, "", userData)); - - vm.prank(testAccount); - hub.safeTransferFrom( - testAccount, STANDARD_TREASURY, uint256(uint160(address(testGroupSafe))), amountToRedeem, data - ); - - address lbp = TestLBPMintPolicy(proxy).getLBPAddress(testAccount); - // 2. burn group token - vm.prank(testAccount); - hub.burn(uint256(uint160(address(testGroupSafe))), amountToBurn, ""); - - vm.prank(testAccount); - TestLBPMintPolicy(proxy).withdrawBPT(); - - // 3. withdraw liquidity - uint256 balance = IERC20(lbp).balanceOf(testAccount); - vm.prank(testAccount); - IERC20(lbp).approve(address(circlesLBPFactory), balance); - vm.prank(testAccount); - circlesLBPFactory.exitLBP(lbp, balance); - } - - function _createLBPGroupMint() internal { - // 1. avatar should wrap into Inflationary CRC - vm.prank(testAccount); - address inflationaryCRC = hub.wrap(testAccount, 37971397492393667509, CirclesType.Inflation); - // 2. approve LBP factory to spend 48 InflCRC - vm.prank(testAccount); - IERC20(inflationaryCRC).approve(address(circlesLBPFactory), 48 ether); - // try to mint before lbp - vm.expectRevert(); - _groupMint(10 ether); - // 3. create lbp - vm.prank(testAccount); - circlesLBPFactory.createLBP{value: 50 ether}(address(testGroupSafe), 0.01 ether, 7 days); - // mint group token - _groupMint(10 ether); - } - - function _groupMint(uint256 amount) internal { - address[] memory collateralAvatars = new address[](1); - collateralAvatars[0] = testAccount; - uint256[] memory amounts = new uint256[](1); - amounts[0] = amount; - vm.prank(testAccount); - hub.groupMint(address(testGroupSafe), collateralAvatars, amounts, ""); - } - - function _executeSafeTx(Safe safe, address to, bytes memory data, Enum.Operation operation) internal { - uint256 nonce = safe.nonce(); - bytes32 txHash = safe.getTransactionHash( - to, // to - 0, // value - data, - operation, - 0, // safeTxGas - 0, // baseGas - 0, // gasPrice - address(0), // gasToken - address(0), // refundReceiver - nonce // nonce - ); - - // Safe flow with approvedHash - uint256 threshold = safe.getThreshold(); - address[] memory owners = safe.getOwners(); - bytes memory signatures; - for (uint256 i; i < threshold;) { - // use owner to send tx - vm.prank(owners[i]); - safe.approveHash(txHash); - // craft signatures - // r s v - bytes memory approvedHashSignature = abi.encodePacked(uint256(uint160(owners[i])), bytes32(0), bytes1(0x01)); - // TODO: need to sort owners first - signatures = bytes.concat(signatures, approvedHashSignature); - unchecked { - ++i; - } - } - - bool success = safe.execTransaction( - to, // to - 0, // value - data, - operation, - 0, // safeTxGas - 0, // baseGas - 0, // gasPrice - address(0), // gasToken - payable(address(0)), // refundReceiver - signatures // signatures - ); - assertTrue(success); - } -}