From 421c37f651868c642acbb4dda270b540436f6384 Mon Sep 17 00:00:00 2001 From: roleengineer Date: Fri, 20 Dec 2024 17:54:18 +0100 Subject: [PATCH 01/24] removed unused code --- ...oduleFactory.s.sol => DeployFactory.s.sol} | 8 +- script/DeployMintPolicy.s.sol | 8 +- src/interfaces/ITestLBPMintPolicy.sol | 7 - src/interfaces/ITestTrustModule.sol | 9 - src/module/TestTrustModule.sol | 78 ----- src/policy/GroupDemurrage.sol | 82 ----- src/policy/TestLBPMintPolicy.sol | 282 ------------------ test/CreateTestProxyLBPMintPolicy.t.sol | 17 +- test/DepositFlow.t.sol | 220 -------------- 9 files changed, 15 insertions(+), 696 deletions(-) rename script/{DeployModuleFactory.s.sol => DeployFactory.s.sol} (57%) delete mode 100644 src/interfaces/ITestLBPMintPolicy.sol delete mode 100644 src/interfaces/ITestTrustModule.sol delete mode 100644 src/module/TestTrustModule.sol delete mode 100644 src/policy/GroupDemurrage.sol delete mode 100644 src/policy/TestLBPMintPolicy.sol delete mode 100644 test/DepositFlow.t.sol diff --git a/script/DeployModuleFactory.s.sol b/script/DeployFactory.s.sol similarity index 57% rename from script/DeployModuleFactory.s.sol rename to script/DeployFactory.s.sol index f8970ef..2c34edd 100644 --- a/script/DeployModuleFactory.s.sol +++ b/script/DeployFactory.s.sol @@ -2,24 +2,20 @@ 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 { +contract DeployFactory is Script { address deployer = address(0x6BF173798733623cc6c221eD52c010472247d861); - TestTrustModule public trustModule; // 0x56652E53649F20C6a360Ea5F25379F9987cECE82 - TestCirclesLBPFactory public circlesLBPFactory; // 0x97030b525248cAc78aabcc33D37139BfB5a34750 + TestCirclesLBPFactory public circlesLBPFactory; 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/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/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/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); - } -} From 3ba6a9c766893c9290176cd09d92986ffa8efdf9 Mon Sep 17 00:00:00 2001 From: roleengineer Date: Fri, 20 Dec 2024 18:39:54 +0100 Subject: [PATCH 02/24] Factory refactored to template state --- src/factory/TestCirclesLBPFactory.sol | 83 +++++++++++++-------------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/src/factory/TestCirclesLBPFactory.sol b/src/factory/TestCirclesLBPFactory.sol index 92b8555..d1ea1a8 100644 --- a/src/factory/TestCirclesLBPFactory.sol +++ b/src/factory/TestCirclesLBPFactory.sol @@ -7,36 +7,35 @@ 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. + * @notice Contract allows to create LBP. + * Contract allows to exit pool. */ contract TestCirclesLBPFactory { - /// Method can be called only by Liquidity Bootstraping Pool owner. - error OnlyLBPOwner(); + /// Method is called by unknown account. + error NotAUser(); + /// Balancer Pool Tokens are still locked. + error TokensLockedUntilTimestamp(uint256 timestamp); /// 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); + /// LBP was created previously, currently only 1 LBP per user can be created. + error OnlyOneLBPPerUser(); /// 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); + event LBPCreated(address indexed user, address indexed lbp); - struct UserGroup { - address user; - address group; + struct LBPData { + address lbp; + uint96 bptUnlockTimestamp; } /// @dev BPT name and symbol prefix. @@ -51,6 +50,10 @@ contract TestCirclesLBPFactory { uint256 internal constant WEIGHT_99 = 0.99 ether; /// @dev LBP token weight 50%. uint256 internal constant WEIGHT_50 = 0.5 ether; + /// @dev Update weight duration. + //uint256 internal constant UPDATE_WEIGHT_DURATION = 365 days; + /// @dev Swap fee percentage is set to 1%. + uint256 internal constant SWAP_FEE = 0.01 ether; /// @notice Balancer v2 Vault. address public constant VAULT = address(0xBA12222222228d8Ba445958a75a0704d566BF2C8); @@ -66,26 +69,20 @@ contract TestCirclesLBPFactory { /// @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; + mapping(address user => LBPData data) public userToLBPData; 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. + /// @param updateWeightDuration is temporary replacement of constant ONE_YEAR for testing flexibility. /// @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 { + function createLBP(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); + // for now only 1 lbp per user + if (userToLBPData[msg.sender].lbp != address(0)) revert OnlyOneLBPPerUser(); // check inflationaryCircles address inflationaryCirlces = LIFT_ERC20.erc20Circles(uint8(1), msg.sender); @@ -117,16 +114,14 @@ contract TestCirclesLBPFactory { _symbol(inflationaryCirlces), tokens, weights, - swapFeePercentage, + SWAP_FEE, 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); + // attach lbp to user + userToLBPData[msg.sender].lbp = lbp; - emit LBPCreated(msg.sender, group, lbp); + emit LBPCreated(msg.sender, lbp); bytes32 poolId = ILBP(lbp).getPoolId(); @@ -140,15 +135,27 @@ contract TestCirclesLBPFactory { IVault(VAULT).joinPool( poolId, address(this), // sender - mintPolicy, // recipient + address(this), // recipient IVault.JoinPoolRequest(tokens, amountsIn, userData, false) ); // update weight gradually - ILBP(lbp).updateWeightsGradually(block.timestamp, block.timestamp + updateWeightDuration, _endWeights()); + uint256 timestampInYear = block.timestamp + updateWeightDuration; + ILBP(lbp).updateWeightsGradually(block.timestamp, timestampInYear, _endWeights()); - // call mint policy to account deposit - ITestLBPMintPolicy(mintPolicy).depositBPT(msg.sender, lbp); + // set bpt unlock + userToLBPData[msg.sender].bptUnlockTimestamp = uint96(timestampInYear); + } + + function withdrawBalancerPoolTokens() external { + uint256 unlockTimestamp = userToLBPData[msg.sender].bptUnlockTimestamp; + if (unlockTimestamp == 0) revert NotAUser(); + if (unlockTimestamp > block.timestamp) revert TokensLockedUntilTimestamp(unlockTimestamp); + userToLBPData[msg.sender].bptUnlockTimestamp = 0; + + IERC20 lbp = IERC20(userToLBPData[msg.sender].lbp); + uint256 bptAmount = lbp.balanceOf(address(this)); + lbp.transfer(msg.sender, bptAmount); } /// @notice General wrapper function over vault.exitPool, allows to extract @@ -178,14 +185,6 @@ contract TestCirclesLBPFactory { ); } - /** - * @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) { From a6391897719cd456ebc89a2381303d3a6bbf4422 Mon Sep 17 00:00:00 2001 From: roleengineer Date: Wed, 8 Jan 2025 11:55:16 +0100 Subject: [PATCH 03/24] added OrderCreator prototype --- script/DeployOrderCreator.t.sol | 46 +++++++++ src/factory/TestCirclesLBPFactory.sol | 1 + src/prototype/OrderCreator.sol | 138 ++++++++++++++++++++++++++ 3 files changed, 185 insertions(+) create mode 100644 script/DeployOrderCreator.t.sol create mode 100644 src/prototype/OrderCreator.sol diff --git a/script/DeployOrderCreator.t.sol b/script/DeployOrderCreator.t.sol new file mode 100644 index 0000000..b3c4da3 --- /dev/null +++ b/script/DeployOrderCreator.t.sol @@ -0,0 +1,46 @@ +// 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"); + } +} + +/* +curl -X 'POST' \ + 'https://api.cow.fi/xdai/api/v1/orders' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "sellToken": "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d", + "buyToken": "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb", + "receiver": "0xeb2EE204c0E15184E4f4a2189d1c07ffb611D635", + "sellAmount": "100000000000000000", + "buyAmount": "1", + "validTo": 1894006860, + "feeAmount": "0", + "kind": "sell", + "partiallyFillable": false, + "sellTokenBalance": "erc20", + "buyTokenBalance": "erc20", + "signingScheme": "presign", + "signature": "0x", + "from": "0xeb2EE204c0E15184E4f4a2189d1c07ffb611D635", + "appData": "{\"version\":\"1.1.0\",\"appCode\":\"Zeal powered by Qantura\",\"metadata\":{\"hooks\":{\"version\":\"0.1.0\",\"post\":[{\"target\":\"0xeb2ee204c0e15184e4f4a2189d1c07ffb611d635\",\"callData\":\"0xbb5ae136\",\"gasLimit\":\"200000\"}]}}}" +}' +*/ \ No newline at end of file diff --git a/src/factory/TestCirclesLBPFactory.sol b/src/factory/TestCirclesLBPFactory.sol index d1ea1a8..ebd0840 100644 --- a/src/factory/TestCirclesLBPFactory.sol +++ b/src/factory/TestCirclesLBPFactory.sol @@ -15,6 +15,7 @@ import {ILBP} from "src/interfaces/ILBP.sol"; * @title Test version of Circles Liquidity Bootstraping Pool Factory. * @notice Contract allows to create LBP. * Contract allows to exit pool. + * Factory should have an admin function to make release of lbp for everyone. */ contract TestCirclesLBPFactory { /// Method is called by unknown account. diff --git a/src/prototype/OrderCreator.sol b/src/prototype/OrderCreator.sol new file mode 100644 index 0000000..2896d55 --- /dev/null +++ b/src/prototype/OrderCreator.sol @@ -0,0 +1,138 @@ +// 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 = 0x7B2e78D4dFaABA045A167a70dA285E30E8FcA196; + 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); + } +} \ No newline at end of file From 6381a99ef2a8faa7da20d851092fc370ade0b6ad Mon Sep 17 00:00:00 2001 From: roleengineer Date: Wed, 8 Jan 2025 12:39:42 +0100 Subject: [PATCH 04/24] renamed factory and added prototype code inside CirclesBacking contract --- script/DeployFactory.s.sol | 8 +- script/DeployOrderCreator.t.sol | 2 +- src/CirclesBacking.sol | 111 ++++++++++++++++++ ...PFactory.sol => CirclesBackingFactory.sol} | 7 +- src/interfaces/ICowswapSettlement.sol | 7 ++ src/interfaces/IGetUid.sol | 17 +++ src/prototype/OrderCreator.sol | 17 +-- 7 files changed, 149 insertions(+), 20 deletions(-) create mode 100644 src/CirclesBacking.sol rename src/factory/{TestCirclesLBPFactory.sol => CirclesBackingFactory.sol} (97%) create mode 100644 src/interfaces/ICowswapSettlement.sol create mode 100644 src/interfaces/IGetUid.sol diff --git a/script/DeployFactory.s.sol b/script/DeployFactory.s.sol index 2c34edd..b8e575b 100644 --- a/script/DeployFactory.s.sol +++ b/script/DeployFactory.s.sol @@ -2,20 +2,20 @@ pragma solidity ^0.8.28; import {Script, console} from "forge-std/Script.sol"; -import {TestCirclesLBPFactory} from "src/factory/TestCirclesLBPFactory.sol"; +import {CirclesBackingFactory} from "src/factory/CirclesBackingFactory.sol"; contract DeployFactory is Script { address deployer = address(0x6BF173798733623cc6c221eD52c010472247d861); - TestCirclesLBPFactory public circlesLBPFactory; + CirclesBackingFactory public circlesBackingFactory; function setUp() public {} function run() public { vm.startBroadcast(deployer); - circlesLBPFactory = new TestCirclesLBPFactory(); + circlesBackingFactory = new CirclesBackingFactory(); vm.stopBroadcast(); - console.log(address(circlesLBPFactory), "CirclesLBPFactory"); + console.log(address(circlesBackingFactory), "CirclesBackingFactory"); } } diff --git a/script/DeployOrderCreator.t.sol b/script/DeployOrderCreator.t.sol index b3c4da3..b7ffbef 100644 --- a/script/DeployOrderCreator.t.sol +++ b/script/DeployOrderCreator.t.sol @@ -43,4 +43,4 @@ curl -X 'POST' \ "from": "0xeb2EE204c0E15184E4f4a2189d1c07ffb611D635", "appData": "{\"version\":\"1.1.0\",\"appCode\":\"Zeal powered by Qantura\",\"metadata\":{\"hooks\":{\"version\":\"0.1.0\",\"post\":[{\"target\":\"0xeb2ee204c0e15184e4f4a2189d1c07ffb611d635\",\"callData\":\"0xbb5ae136\",\"gasLimit\":\"200000\"}]}}}" }' -*/ \ No newline at end of file +*/ diff --git a/src/CirclesBacking.sol b/src/CirclesBacking.sol new file mode 100644 index 0000000..a1949bc --- /dev/null +++ b/src/CirclesBacking.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.28; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IGetUid} from "src/interfaces/IGetUid.sol"; +import {ICowswapSettlement} from "src/interfaces/ICowswapSettlement.sol"; + +contract CirclesBacking { + 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 = 0x7B2e78D4dFaABA045A167a70dA285E30E8FcA196; + 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/src/factory/TestCirclesLBPFactory.sol b/src/factory/CirclesBackingFactory.sol similarity index 97% rename from src/factory/TestCirclesLBPFactory.sol rename to src/factory/CirclesBackingFactory.sol index ebd0840..1083bf3 100644 --- a/src/factory/TestCirclesLBPFactory.sol +++ b/src/factory/CirclesBackingFactory.sol @@ -12,12 +12,11 @@ import {INoProtocolFeeLiquidityBootstrappingPoolFactory} from "src/interfaces/IL import {ILBP} from "src/interfaces/ILBP.sol"; /** - * @title Test version of Circles Liquidity Bootstraping Pool Factory. - * @notice Contract allows to create LBP. - * Contract allows to exit pool. + * @title Circles Backing Factory. + * @notice Contract allows to create CircleBacking instances. * Factory should have an admin function to make release of lbp for everyone. */ -contract TestCirclesLBPFactory { +contract CirclesBackingFactory { /// Method is called by unknown account. error NotAUser(); /// Balancer Pool Tokens are still locked. 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/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/prototype/OrderCreator.sol b/src/prototype/OrderCreator.sol index 2896d55..e5165d5 100644 --- a/src/prototype/OrderCreator.sol +++ b/src/prototype/OrderCreator.sol @@ -46,16 +46,15 @@ contract OrderCreator { 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 + 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'; + str[0] = "0"; + str[1] = "x"; for (uint256 i = 0; i < 20; i++) { str[2 + i * 2] = alphabet[uint8(value[i + 12] >> 4)]; @@ -67,11 +66,7 @@ contract OrderCreator { function getAppData(address _newAccount) public pure returns (bytes32) { string memory _newAccountStr = addressToString(_newAccount); - string memory _appDataStr = string.concat( - preAppData, - _newAccountStr, - postAppData - ); + string memory _appDataStr = string.concat(preAppData, _newAccountStr, postAppData); return keccak256(bytes(_appDataStr)); } @@ -90,7 +85,7 @@ contract OrderCreator { // Generate order UID using the "getUid" contract IGetUid getUidContract = IGetUid(GET_UID_CONTRACT); - (bytes32 orderDigest, ) = getUidContract.getUid( + (bytes32 orderDigest,) = getUidContract.getUid( WXDAI, GNO, address(this), // Use contract address as the receiver @@ -135,4 +130,4 @@ contract OrderCreator { // Emit event for the transfer emit GnoTransferred(gnoBalance, RECEIVER); } -} \ No newline at end of file +} From eaaefe984df932a0a15862238a56c12d79524222 Mon Sep 17 00:00:00 2001 From: roleengineer Date: Wed, 8 Jan 2025 12:55:32 +0100 Subject: [PATCH 05/24] forge install: contracts v1.7.0 --- .gitmodules | 3 +++ lib/contracts | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/contracts 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 From bf708a81e8cadfd0c0d03e4178717051db96bda3 Mon Sep 17 00:00:00 2001 From: roleengineer Date: Wed, 8 Jan 2025 17:01:50 +0100 Subject: [PATCH 06/24] added factory prototype and start refactoring --- remappings.txt | 1 + src/CirclesBacking.sol | 69 +++++++------------ src/factory/CirclesBackingFactory.sol | 96 +++++++++++++++++++++++++++ src/interfaces/IFactory.sol | 9 +++ src/prototype/Factory.sol | 45 +++++++++++++ 5 files changed, 174 insertions(+), 46 deletions(-) create mode 100644 src/interfaces/IFactory.sol create mode 100644 src/prototype/Factory.sol 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/src/CirclesBacking.sol b/src/CirclesBacking.sol index a1949bc..cc46885 100644 --- a/src/CirclesBacking.sol +++ b/src/CirclesBacking.sol @@ -4,61 +4,41 @@ pragma solidity ^0.8.28; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IGetUid} from "src/interfaces/IGetUid.sol"; import {ICowswapSettlement} from "src/interfaces/ICowswapSettlement.sol"; +import {IFactory} from "src/interfaces/IFactory.sol"; // temporary solution contract CirclesBacking { - address public constant WXDAI = 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d; - address public constant GNO = 0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb; - address public constant GET_UID_CONTRACT = 0xCA51403B524dF7dA6f9D6BFc64895AD833b5d711; + address public constant USDC = 0x2a22f9c3b484c3629090FeED35F17Ff8F88f76F0; + uint256 public constant USDC_DECIMALS = 1e6; + uint256 public constant TRADE_AMOUNT = 100 * USDC_DECIMALS; + uint32 public constant VALID_TO = uint32(1894006860); // timestamp in 5 years + address public constant COWSWAP_SETTLEMENT_CONTRACT = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; - address public constant RECEIVER = 0x7B2e78D4dFaABA045A167a70dA285E30E8FcA196; address public constant VAULT_RELAY = 0xC92E8bdf79f0507f65a392b0ab4667716BFE0110; + address public constant GET_UID_CONTRACT = 0xCA51403B524dF7dA6f9D6BFc64895AD833b5d711; - 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); + address public constant WXDAI = 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d; + address public constant GNO = 0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb; + + address public backer; + address public backingAsset; + address public personalCircles; + bytes32 public appData; 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); + constructor(address _backer, address _backingAsset, address _personalCircles) { + backer = _backer; + backingAsset = _backingAsset; + personalCircles = _personalCircles; + (, bytes32 appDataHash) = IFactory(msg.sender).getAppData(address(this)); // temporary solution + appData = appDataHash; } function createOrder() external { - // Approve wxDAI to Vault Relay contract - IERC20(WXDAI).approve(VAULT_RELAY, TRADE_AMOUNT); - - // Generate appData dynamically - bytes32 appData = getAppData(address(this)); + // Approve USDC to Vault Relay contract + IERC20(USDC).approve(VAULT_RELAY, TRADE_AMOUNT); // Generate order UID using the "getUid" contract IGetUid getUidContract = IGetUid(GET_UID_CONTRACT); @@ -102,10 +82,7 @@ contract CirclesBacking { require(gnoBalance > 0, "No GNO balance to transfer"); // Transfer GNO to the receiver - bool success = IERC20(GNO).transfer(RECEIVER, gnoBalance); + bool success = IERC20(GNO).transfer(backer, gnoBalance); require(success, "GNO transfer failed"); - - // Emit event for the transfer - emit GnoTransferred(gnoBalance, RECEIVER); } } diff --git a/src/factory/CirclesBackingFactory.sol b/src/factory/CirclesBackingFactory.sol index 1083bf3..d7b4450 100644 --- a/src/factory/CirclesBackingFactory.sol +++ b/src/factory/CirclesBackingFactory.sol @@ -10,6 +10,7 @@ import {IVault} from "src/interfaces/IVault.sol"; import {ILiftERC20} from "src/interfaces/ILiftERC20.sol"; import {INoProtocolFeeLiquidityBootstrappingPoolFactory} from "src/interfaces/ILBPFactory.sol"; import {ILBP} from "src/interfaces/ILBP.sol"; +import {CirclesBacking} from "src/CirclesBacking.sol"; /** * @title Circles Backing Factory. @@ -17,6 +18,8 @@ import {ILBP} from "src/interfaces/ILBP.sol"; * Factory should have an admin function to make release of lbp for everyone. */ contract CirclesBackingFactory { + /// Circles backing does not support `requestedAsset` asset. + error UnsupportedBackingAsset(address requestedAsset); /// Method is called by unknown account. error NotAUser(); /// Balancer Pool Tokens are still locked. @@ -33,6 +36,9 @@ contract CirclesBackingFactory { /// @notice Emitted when a LBP is created. event LBPCreated(address indexed user, address indexed lbp); + /// @notice Emitted when a CirclesBacking is created. + event CirclesBackingDeployed(address indexed deployedAddress, address indexed backer); + struct LBPData { address lbp; uint96 bptUnlockTimestamp; @@ -71,8 +77,98 @@ contract CirclesBackingFactory { mapping(address user => LBPData data) public userToLBPData; + 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":"0xbb5ae136","gasLimit":"200000"}]}}}'; // Updated calldata for checkOrderFilledAndTransfer + + mapping(address supportedAsset => bool) public supportedBackingAssets; + constructor() {} + function startBacking(address backingAsset) external { + if (!supportedBackingAssets[backingAsset]) revert UnsupportedBackingAsset(backingAsset); + address personalCirclesAddress = getPersonalCircles(msg.sender); + address instance = deployCirclesBacking(msg.sender, backingAsset, personalCirclesAddress); + } + + // personal circles + + function getPersonalCircles(address avatar) public view returns (address inflationaryCircles) { + inflationaryCircles = LIFT_ERC20.erc20Circles(uint8(1), avatar); + if (inflationaryCircles == address(0)) revert InflationaryCirclesNotExists(avatar); + } + + // cowswap app data + + 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)); + } + + 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); + } + + // deploy instance + /** + * @notice Deploys a new CirclesBacking contract with CREATE2. + * @param backingAsset Address which will be used to back circles. + * @param backer Address which is backing circles. + * @return deployedAddress Address of the deployed contract. + */ + function deployCirclesBacking(address backer, address backingAsset, address personalCircles) + internal + returns (address deployedAddress) + { + bytes32 salt = keccak256(abi.encodePacked(backer)); + bytes memory bytecode = + abi.encodePacked(type(CirclesBacking).creationCode, abi.encode(backer, backingAsset, personalCircles)); + + assembly { + deployedAddress := create2(0, add(bytecode, 0x20), mload(bytecode), salt) + if iszero(extcodesize(deployedAddress)) { revert(0, 0) } + } + + emit CirclesBackingDeployed(deployedAddress, backer); + } + + // counterfactual + /** + * @notice Computes the deterministic address for CirclesBacking contract. + * @param backingAsset Asset which will be used to back circles. + * @param backer Address which is backing circles. + * @return predictedAddress Predicted address of the deployed contract. + */ + function computeAddress(address backer, address backingAsset, address personalCircles) + external + view + returns (address predictedAddress) + { + bytes32 salt = keccak256(abi.encodePacked(backer)); + bytes memory bytecode = + abi.encodePacked(type(CirclesBacking).creationCode, abi.encode(backer, backingAsset, personalCircles)); + predictedAddress = address( + uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(bytecode))))) + ); + } + // LBP Factory logic /// @notice Creates LBP with underlying assets: `XDAI_AMOUNT` SxDAI and `CRC_AMOUNT` InflationaryCircles. diff --git a/src/interfaces/IFactory.sol b/src/interfaces/IFactory.sol new file mode 100644 index 0000000..74b078c --- /dev/null +++ b/src/interfaces/IFactory.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.28; + +interface IFactory { + function getAppData(address _circlesBackingInstance) + external + pure + returns (string memory appDataString, bytes32 appDataHash); +} 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))))) + ); + } +} From 78e5461b2511572b902d769129793889d4d81c03 Mon Sep 17 00:00:00 2001 From: roleengineer Date: Wed, 8 Jan 2025 18:34:22 +0100 Subject: [PATCH 07/24] move init and order preparation logic to factory --- src/CirclesBacking.sol | 60 +++++++++------------------ src/factory/CirclesBackingFactory.sol | 51 ++++++++++++++++------- 2 files changed, 55 insertions(+), 56 deletions(-) diff --git a/src/CirclesBacking.sol b/src/CirclesBacking.sol index cc46885..715885e 100644 --- a/src/CirclesBacking.sol +++ b/src/CirclesBacking.sol @@ -2,62 +2,42 @@ pragma solidity ^0.8.28; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IGetUid} from "src/interfaces/IGetUid.sol"; import {ICowswapSettlement} from "src/interfaces/ICowswapSettlement.sol"; import {IFactory} from "src/interfaces/IFactory.sol"; // temporary solution contract CirclesBacking { - address public constant USDC = 0x2a22f9c3b484c3629090FeED35F17Ff8F88f76F0; - uint256 public constant USDC_DECIMALS = 1e6; - uint256 public constant TRADE_AMOUNT = 100 * USDC_DECIMALS; - uint32 public constant VALID_TO = uint32(1894006860); // timestamp in 5 years + /// Already initialized. + error AlreadyInitialized(); address public constant COWSWAP_SETTLEMENT_CONTRACT = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; address public constant VAULT_RELAY = 0xC92E8bdf79f0507f65a392b0ab4667716BFE0110; - address public constant GET_UID_CONTRACT = 0xCA51403B524dF7dA6f9D6BFc64895AD833b5d711; - - address public constant WXDAI = 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d; - address public constant GNO = 0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb; address public backer; address public backingAsset; address public personalCircles; - bytes32 public appData; bytes public storedOrderUid; event OrderCreated(bytes32 orderHash); - constructor(address _backer, address _backingAsset, address _personalCircles) { + constructor() {} + + function initAndCreateOrder( + address _backer, + address _backingAsset, + address _personalCircles, + bytes memory orderUid, + address usdc, + uint256 tradeAmount + ) external { + if (backer != address(0)) revert AlreadyInitialized(); + // init backer = _backer; backingAsset = _backingAsset; personalCircles = _personalCircles; - (, bytes32 appDataHash) = IFactory(msg.sender).getAppData(address(this)); // temporary solution - appData = appDataHash; - } - function createOrder() external { // Approve USDC to Vault Relay contract - IERC20(USDC).approve(VAULT_RELAY, TRADE_AMOUNT); - - // 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)); + IERC20(usdc).approve(VAULT_RELAY, tradeAmount); // Store the order UID storedOrderUid = orderUid; @@ -67,7 +47,7 @@ contract CirclesBacking { cowswapSettlement.setPreSignature(orderUid, true); // Emit event with the order UID - emit OrderCreated(orderDigest); + //emit OrderCreated(orderDigest); } function checkOrderFilledAndTransfer() public { @@ -78,11 +58,11 @@ contract CirclesBacking { 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"); + //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(backer, gnoBalance); - require(success, "GNO transfer failed"); + //bool success = IERC20(GNO).transfer(backer, gnoBalance); + //require(success, "GNO transfer failed"); } } diff --git a/src/factory/CirclesBackingFactory.sol b/src/factory/CirclesBackingFactory.sol index d7b4450..e8df13b 100644 --- a/src/factory/CirclesBackingFactory.sol +++ b/src/factory/CirclesBackingFactory.sol @@ -11,6 +11,7 @@ import {ILiftERC20} from "src/interfaces/ILiftERC20.sol"; import {INoProtocolFeeLiquidityBootstrappingPoolFactory} from "src/interfaces/ILBPFactory.sol"; import {ILBP} from "src/interfaces/ILBP.sol"; import {CirclesBacking} from "src/CirclesBacking.sol"; +import {IGetUid} from "src/interfaces/IGetUid.sol"; /** * @title Circles Backing Factory. @@ -44,6 +45,13 @@ contract CirclesBackingFactory { uint96 bptUnlockTimestamp; } + // order constants + address public constant GET_UID_CONTRACT = 0xCA51403B524dF7dA6f9D6BFc64895AD833b5d711; + address public constant USDC = 0x2a22f9c3b484c3629090FeED35F17Ff8F88f76F0; + uint256 public constant USDC_DECIMALS = 1e6; + uint256 public constant TRADE_AMOUNT = 100 * USDC_DECIMALS; + uint32 public constant VALID_TO = uint32(1894006860); // timestamp in 5 years + /// @dev BPT name and symbol prefix. string internal constant LBP_PREFIX = "testLBP-"; /// @notice Amount of xDai to use in LBP initial liquidity. @@ -88,7 +96,29 @@ contract CirclesBackingFactory { function startBacking(address backingAsset) external { if (!supportedBackingAssets[backingAsset]) revert UnsupportedBackingAsset(backingAsset); address personalCirclesAddress = getPersonalCircles(msg.sender); - address instance = deployCirclesBacking(msg.sender, backingAsset, personalCirclesAddress); + address instance = deployCirclesBacking(msg.sender); + + // create order + (, bytes32 appData) = getAppData(instance); + // Generate order UID using the "getUid" contract + IGetUid getUidContract = IGetUid(GET_UID_CONTRACT); + (bytes32 orderDigest,) = getUidContract.getUid( + USDC, + backingAsset, + instance, // 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, instance, uint32(VALID_TO)); + CirclesBacking(instance).initAndCreateOrder( + msg.sender, backingAsset, personalCirclesAddress, orderUid, USDC, TRADE_AMOUNT + ); } // personal circles @@ -129,17 +159,12 @@ contract CirclesBackingFactory { // deploy instance /** * @notice Deploys a new CirclesBacking contract with CREATE2. - * @param backingAsset Address which will be used to back circles. * @param backer Address which is backing circles. * @return deployedAddress Address of the deployed contract. */ - function deployCirclesBacking(address backer, address backingAsset, address personalCircles) - internal - returns (address deployedAddress) - { + function deployCirclesBacking(address backer) internal returns (address deployedAddress) { bytes32 salt = keccak256(abi.encodePacked(backer)); - bytes memory bytecode = - abi.encodePacked(type(CirclesBacking).creationCode, abi.encode(backer, backingAsset, personalCircles)); + bytes memory bytecode = type(CirclesBacking).creationCode; assembly { deployedAddress := create2(0, add(bytecode, 0x20), mload(bytecode), salt) @@ -152,18 +177,12 @@ contract CirclesBackingFactory { // counterfactual /** * @notice Computes the deterministic address for CirclesBacking contract. - * @param backingAsset Asset which will be used to back circles. * @param backer Address which is backing circles. * @return predictedAddress Predicted address of the deployed contract. */ - function computeAddress(address backer, address backingAsset, address personalCircles) - external - view - returns (address predictedAddress) - { + function computeAddress(address backer) external view returns (address predictedAddress) { bytes32 salt = keccak256(abi.encodePacked(backer)); - bytes memory bytecode = - abi.encodePacked(type(CirclesBacking).creationCode, abi.encode(backer, backingAsset, personalCircles)); + bytes memory bytecode = type(CirclesBacking).creationCode; predictedAddress = address( uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(bytecode))))) ); From 52d14ccc24aa2639b9eccc2f2ed00e105fd89a8a Mon Sep 17 00:00:00 2001 From: roleengineer Date: Fri, 10 Jan 2025 00:34:59 +0100 Subject: [PATCH 08/24] continue refactoring --- ...Creator.t.sol => DeployOrderCreator.s.sol} | 6 +- src/CirclesBacking.sol | 88 ++++-- src/factory/CirclesBackingFactory.sol | 259 +++++++++--------- src/interfaces/IFactory.sol | 3 + 4 files changed, 204 insertions(+), 152 deletions(-) rename script/{DeployOrderCreator.t.sol => DeployOrderCreator.s.sol} (88%) diff --git a/script/DeployOrderCreator.t.sol b/script/DeployOrderCreator.s.sol similarity index 88% rename from script/DeployOrderCreator.t.sol rename to script/DeployOrderCreator.s.sol index b7ffbef..9d30928 100644 --- a/script/DeployOrderCreator.t.sol +++ b/script/DeployOrderCreator.s.sol @@ -29,7 +29,7 @@ curl -X 'POST' \ -d '{ "sellToken": "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d", "buyToken": "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb", - "receiver": "0xeb2EE204c0E15184E4f4a2189d1c07ffb611D635", + "receiver": "0xEA39b6F8F98f91ECCe24A5601FD02DF850d3eC3E", "sellAmount": "100000000000000000", "buyAmount": "1", "validTo": 1894006860, @@ -40,7 +40,7 @@ curl -X 'POST' \ "buyTokenBalance": "erc20", "signingScheme": "presign", "signature": "0x", - "from": "0xeb2EE204c0E15184E4f4a2189d1c07ffb611D635", - "appData": "{\"version\":\"1.1.0\",\"appCode\":\"Zeal powered by Qantura\",\"metadata\":{\"hooks\":{\"version\":\"0.1.0\",\"post\":[{\"target\":\"0xeb2ee204c0e15184e4f4a2189d1c07ffb611d635\",\"callData\":\"0xbb5ae136\",\"gasLimit\":\"200000\"}]}}}" + "from": "0xEA39b6F8F98f91ECCe24A5601FD02DF850d3eC3E", + "appData": "{\"version\":\"1.1.0\",\"appCode\":\"Zeal powered by Qantura\",\"metadata\":{\"hooks\":{\"version\":\"0.1.0\",\"post\":[{\"target\":\"0xea39b6f8f98f91ecce24a5601fd02df850d3ec3e\",\"callData\":\"0xbb5ae136\",\"gasLimit\":\"200000\"}]}}}" }' */ diff --git a/src/CirclesBacking.sol b/src/CirclesBacking.sol index 715885e..2697669 100644 --- a/src/CirclesBacking.sol +++ b/src/CirclesBacking.sol @@ -4,25 +4,47 @@ 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"; // temporary solution +import {ILBP} from "src/interfaces/ILBP.sol"; contract CirclesBacking { /// Already initialized. error AlreadyInitialized(); - - address public constant COWSWAP_SETTLEMENT_CONTRACT = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; + /// Function must be called only by Cowswap posthook. + error OrderNotFilledYet(); + /// Cowswap solver must transfer the swap result before calling posthook. + error InsufficientBackingAssetBalance(); + /// Unauthorized access. + error NotBacker(); + /// Balancer Pool Tokens are still locked. + error TokensLockedUntilTimestamp(uint256 timestamp); + + ICowswapSettlement public constant COWSWAP_SETTLEMENT = + ICowswapSettlement(address(0x9008D19f58AAbD9eD0D60971565AA8510560ab41)); address public constant VAULT_RELAY = 0xC92E8bdf79f0507f65a392b0ab4667716BFE0110; + address public constant VAULT_BALANCER = address(0xBA12222222228d8Ba445958a75a0704d566BF2C8); + IFactory internal immutable FACTORY; + /// @notice Amount of InflationaryCircles to use in LBP initial liquidity. + uint256 public constant CRC_AMOUNT = 48 ether; + /// @dev LBP token weight 50%. + uint256 internal constant WEIGHT_50 = 0.5 ether; + /// @dev Update weight duration is set to 1 year. + uint256 internal constant UPDATE_WEIGHT_DURATION = 365 days; address public backer; address public backingAsset; address public personalCircles; + address public lbp; + uint256 public balancerPoolTokensUnlockTimestamp; bytes public storedOrderUid; - event OrderCreated(bytes32 orderHash); + event OrderCreated(bytes orderUid); - constructor() {} + constructor() { + FACTORY = IFactory(msg.sender); + } - function initAndCreateOrder( + function initiateBacking( address _backer, address _backingAsset, address _personalCircles, @@ -43,26 +65,56 @@ contract CirclesBacking { storedOrderUid = orderUid; // Place the order using "setPreSignature" - ICowswapSettlement cowswapSettlement = ICowswapSettlement(COWSWAP_SETTLEMENT_CONTRACT); - cowswapSettlement.setPreSignature(orderUid, true); + COWSWAP_SETTLEMENT.setPreSignature(orderUid, true); // Emit event with the order UID - //emit OrderCreated(orderDigest); + emit OrderCreated(orderUid); } - function checkOrderFilledAndTransfer() public { + function createLBP() external { // Check if the order has been filled on the CowSwap settlement contract - ICowswapSettlement cowswapSettlement = ICowswapSettlement(COWSWAP_SETTLEMENT_CONTRACT); - uint256 filledAmount = cowswapSettlement.filledAmount(storedOrderUid); + uint256 filledAmount = COWSWAP_SETTLEMENT.filledAmount(storedOrderUid); + if (filledAmount == 0) revert OrderNotFilledYet(); + + // Backing asset balance of the contract + uint256 backingAssetBalance = IERC20(backingAsset).balanceOf(address(this)); + if (backingAssetBalance == 0) revert InsufficientBackingAssetBalance(); + + // Create LBP + // approve vault + IERC20(personalCircles).approve(VAULT_BALANCER, backingAssetBalance); + IERC20(backingAsset).approve(VAULT_BALANCER, CRC_AMOUNT); + + lbp = FACTORY.createLBP(personalCircles, backingAsset, backingAssetBalance); + + // update weight gradually + uint256 timestampInYear = block.timestamp + UPDATE_WEIGHT_DURATION; + ILBP(lbp).updateWeightsGradually(block.timestamp, timestampInYear, _endWeights()); - require(filledAmount > 0, "Order not filled yet"); + // set bpt unlock + balancerPoolTokensUnlockTimestamp = timestampInYear; - // Check GNO balance of the contract - //uint256 gnoBalance = IERC20(GNO).balanceOf(address(this)); - //require(gnoBalance > 0, "No GNO balance to transfer"); + // need to lock, so only 1 call + } + + function claimBalancerPoolTokens() external { + if (msg.sender != backer) revert NotBacker(); + /* + if () + + if (unlockTimestamp == 0) revert NotAUser(); + if (unlockTimestamp > block.timestamp) revert TokensLockedUntilTimestamp(unlockTimestamp); + userToLBPData[msg.sender].bptUnlockTimestamp = 0; + + IERC20 lbp = IERC20(userToLBPData[msg.sender].lbp); + uint256 bptAmount = lbp.balanceOf(address(this)); + lbp.transfer(msg.sender, bptAmount); + */ + } - // Transfer GNO to the receiver - //bool success = IERC20(GNO).transfer(backer, gnoBalance); - //require(success, "GNO transfer failed"); + 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 index e8df13b..ac1ffe8 100644 --- a/src/factory/CirclesBackingFactory.sol +++ b/src/factory/CirclesBackingFactory.sol @@ -3,15 +3,13 @@ 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 {IGetUid} from "src/interfaces/IGetUid.sol"; import {IVault} from "src/interfaces/IVault.sol"; -import {ILiftERC20} from "src/interfaces/ILiftERC20.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"; -import {IGetUid} from "src/interfaces/IGetUid.sol"; /** * @title Circles Backing Factory. @@ -21,111 +19,140 @@ import {IGetUid} from "src/interfaces/IGetUid.sol"; contract CirclesBackingFactory { /// Circles backing does not support `requestedAsset` asset. error UnsupportedBackingAsset(address requestedAsset); - /// Method is called by unknown account. - error NotAUser(); - /// Balancer Pool Tokens are still locked. - error TokensLockedUntilTimestamp(uint256 timestamp); + /// 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(); /// Method requires exact `requiredXDai` xDai amount, was provided: `providedXDai`. error NotExactXDaiAmount(uint256 providedXDai, uint256 requiredXDai); /// LBP was created previously, currently only 1 LBP per user can be created. error OnlyOneLBPPerUser(); - /// 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 lbp); - /// @notice Emitted when a CirclesBacking is created. - event CirclesBackingDeployed(address indexed deployedAddress, address indexed backer); - - struct LBPData { - address lbp; - uint96 bptUnlockTimestamp; - } - - // order constants - address public constant GET_UID_CONTRACT = 0xCA51403B524dF7dA6f9D6BFc64895AD833b5d711; + event CirclesBackingDeployed(address indexed backer, address indexed circlesBackingInstance); + /// @notice Emitted when a LBP is created. + event LBPCreated(address indexed circlesBackingInstance, address indexed lbp); + event CirclesBackingCompleted( + address indexed backer, + address indexed backingAsset, + address indexed circlesBackingInstance, + address lbp, + address personalCRC + ); + + // 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 or for LBP initial liquidity in case USDC.e is backing asset. uint256 public constant TRADE_AMOUNT = 100 * USDC_DECIMALS; - uint32 public constant VALID_TO = uint32(1894006860); // timestamp in 5 years + /// @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":"200000"}]}}}'; // Updated calldata for createLBP - /// @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; + /// 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 LBP token weight 50%. - uint256 internal constant WEIGHT_50 = 0.5 ether; - /// @dev Update weight duration. - //uint256 internal constant UPDATE_WEIGHT_DURATION = 365 days; /// @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-"; - /// @notice Balancer v2 Vault. - address public constant VAULT = address(0xBA12222222228d8Ba445958a75a0704d566BF2C8); - /// @notice Balancer v2 LBPFactory. - INoProtocolFeeLiquidityBootstrappingPoolFactory public constant LBP_FACTORY = - INoProtocolFeeLiquidityBootstrappingPoolFactory(address(0x85a80afee867aDf27B50BdB7b76DA70f1E853062)); + // 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 Wrapped xDAI contract. - IWXDAI public constant WXDAI = IWXDAI(address(0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d)); - /// @notice Savings xDAI contract. - ISXDAI public constant SXDAI = ISXDAI(address(0xaf204776c7245bF4147c2612BF6e5972Ee483701)); - - mapping(address user => LBPData data) public userToLBPData; - - 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":"0xbb5ae136","gasLimit":"200000"}]}}}'; // Updated calldata for checkOrderFilledAndTransfer + /// @notice Amount of InflationaryCircles to use in LBP initial liquidity. + uint256 public constant CRC_AMOUNT = 48 ether; mapping(address supportedAsset => bool) public supportedBackingAssets; + mapping(address circleBacking => address backer) public backerOf; + + constructor() { + 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 + supportedBackingAssets[USDC] = true; // USDC + } - constructor() {} - + // @dev Required upfront approval of this contract for CRC and USDC.e function startBacking(address backingAsset) external { if (!supportedBackingAssets[backingAsset]) revert UnsupportedBackingAsset(backingAsset); - address personalCirclesAddress = getPersonalCircles(msg.sender); + address instance = deployCirclesBacking(msg.sender); + address personalCirclesAddress = getPersonalCircles(msg.sender); + // handling personal CRC: 1. try to get Inflationary, if fails 2. try to get 1155 and wrap, if fails revert + try IERC20(personalCirclesAddress).transferFrom(msg.sender, instance, CRC_AMOUNT) {} + catch { + try HUB_V2.safeTransferFrom(msg.sender, address(this), uint256(uint160(msg.sender)), CRC_AMOUNT, "") { + // NOTE: for now this flow always reverts as not fully implemented + // Reason why not implemented except lack of time is that we might make startBacking internal function called inside + // IERC1155Receiver.onERC1155Received and the whole handling personal CRC flow will be refactored. + // TODO: + // 0. implement IERC1155Receiver.onERC1155Received here + // 1. define the exact erc1155 circles amount to get based on constant of erc20 inflationary constant + // 2. call wrap on HUB_V2 with the exact erc1155 circles amount and type = 1 (infationary) + // 3. check erc20 inflationary balance of address(this) equal CRC_AMOUNT + // 4. transfer to instance + } catch { + revert PersonalCirclesApprovalIsMissing(); + } + } + + // handling USDC.e + IERC20(USDC).transferFrom(msg.sender, instance, TRADE_AMOUNT); + // create order (, bytes32 appData) = getAppData(instance); // Generate order UID using the "getUid" contract - IGetUid getUidContract = IGetUid(GET_UID_CONTRACT); - (bytes32 orderDigest,) = getUidContract.getUid( - USDC, - backingAsset, - instance, // Use contract address as the receiver - TRADE_AMOUNT, - 1, // Determined by off-chain logic or Cowswap solvers - VALID_TO, // ValidTo timestamp - appData, + (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)); - CirclesBacking(instance).initAndCreateOrder( + // Initiate backing + CirclesBacking(instance).initiateBacking( msg.sender, backingAsset, personalCirclesAddress, orderUid, USDC, TRADE_AMOUNT ); } // personal circles + // @dev this call will revert, if avatar is not registered as human or group in Hub contract function getPersonalCircles(address avatar) public view returns (address inflationaryCircles) { inflationaryCircles = LIFT_ERC20.erc20Circles(uint8(1), avatar); - if (inflationaryCircles == address(0)) revert InflationaryCirclesNotExists(avatar); + // TODO: find capacity to understand why i had this revert + //if (inflationaryCircles == address(0)) revert InflationaryCirclesNotExists(avatar); } // cowswap app data @@ -163,15 +190,20 @@ contract CirclesBackingFactory { * @return deployedAddress Address of the deployed contract. */ function deployCirclesBacking(address backer) internal returns (address deployedAddress) { - bytes32 salt = keccak256(abi.encodePacked(backer)); - bytes memory bytecode = type(CirclesBacking).creationCode; + // open question: do we want backer to be able to create only one backing? - this is how it is now. + // or we allow backer to create multiple backings, 1 per supported backing asset - need to add backing asset to salt. + bytes32 salt_ = keccak256(abi.encodePacked(backer)); + + deployedAddress = address(new CirclesBacking{salt: salt_}()); - assembly { - deployedAddress := create2(0, add(bytecode, 0x20), mload(bytecode), salt) - if iszero(extcodesize(deployedAddress)) { revert(0, 0) } + if (deployedAddress == address(0) || deployedAddress.code.length == 0) { + revert CirclesBackingDeploymentFailed(backer); } - emit CirclesBackingDeployed(deployedAddress, backer); + // link instance to backer + backerOf[deployedAddress] = backer; + + emit CirclesBackingDeployed(backer, deployedAddress); } // counterfactual @@ -188,89 +220,60 @@ contract CirclesBackingFactory { ); } - // LBP Factory logic - - /// @notice Creates LBP with underlying assets: `XDAI_AMOUNT` SxDAI and `CRC_AMOUNT` InflationaryCircles. - /// @param updateWeightDuration is temporary replacement of constant ONE_YEAR for testing flexibility. - /// @dev Required InflationaryCircles approval at least `CRC_AMOUNT` before call - function createLBP(uint256 updateWeightDuration) external payable { - // check msg.value - if (msg.value != XDAI_AMOUNT) revert NotExactXDaiAmount(msg.value, XDAI_AMOUNT); - // for now only 1 lbp per user - if (userToLBPData[msg.sender].lbp != address(0)) revert OnlyOneLBPPerUser(); - - // 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); + // admin logic + // TODO + + // LBP logic + + /// @notice Creates LBP with underlying assets: `backingAssetAmount` backingAsset(`backingAsset`) and `CRC_AMOUNT` InflationaryCircles(`personalCRC`). + /// @param personalCRC . + function createLBP(address personalCRC, address backingAsset, uint256 backingAssetAmount) + external + returns (address lbp) + { + address backer = backerOf[msg.sender]; + if (backer == address(0)) revert OnlyCirclesBacking(); // 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)); + 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 - address lbp = LBP_FACTORY.create( - _name(inflationaryCirlces), - _symbol(inflationaryCirlces), + lbp = LBP_FACTORY.create( + _name(personalCRC), + _symbol(personalCRC), tokens, weights, SWAP_FEE, - address(this), // lbp owner + msg.sender, // lbp owner true // enable swap on start ); - // attach lbp to user - userToLBPData[msg.sender].lbp = lbp; - emit LBPCreated(msg.sender, lbp); + emit LBPCreated(backer, lbp); bytes32 poolId = ILBP(lbp).getPoolId(); uint256[] memory amountsIn = new uint256[](2); - amountsIn[0] = tokenZero ? CRC_AMOUNT : shares; - amountsIn[1] = tokenZero ? shares : CRC_AMOUNT; + amountsIn[0] = tokenZero ? CRC_AMOUNT : backingAssetAmount; + amountsIn[1] = tokenZero ? backingAssetAmount : CRC_AMOUNT; bytes memory userData = abi.encode(ILBP.JoinKind.INIT, amountsIn); - + // CHECK: only owner can join pool, however it looks like anyone can do this call setting owner address as sender // provide liquidity into lbp IVault(VAULT).joinPool( poolId, - address(this), // sender - address(this), // recipient + msg.sender, // sender + msg.sender, // recipient IVault.JoinPoolRequest(tokens, amountsIn, userData, false) ); - // update weight gradually - uint256 timestampInYear = block.timestamp + updateWeightDuration; - ILBP(lbp).updateWeightsGradually(block.timestamp, timestampInYear, _endWeights()); - - // set bpt unlock - userToLBPData[msg.sender].bptUnlockTimestamp = uint96(timestampInYear); - } - - function withdrawBalancerPoolTokens() external { - uint256 unlockTimestamp = userToLBPData[msg.sender].bptUnlockTimestamp; - if (unlockTimestamp == 0) revert NotAUser(); - if (unlockTimestamp > block.timestamp) revert TokensLockedUntilTimestamp(unlockTimestamp); - userToLBPData[msg.sender].bptUnlockTimestamp = 0; - - IERC20 lbp = IERC20(userToLBPData[msg.sender].lbp); - uint256 bptAmount = lbp.balanceOf(address(this)); - lbp.transfer(msg.sender, bptAmount); + emit CirclesBackingCompleted(backer, backingAsset, msg.sender, lbp, personalCRC); } /// @notice General wrapper function over vault.exitPool, allows to extract @@ -309,10 +312,4 @@ contract CirclesBackingFactory { 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/IFactory.sol b/src/interfaces/IFactory.sol index 74b078c..e1d6a17 100644 --- a/src/interfaces/IFactory.sol +++ b/src/interfaces/IFactory.sol @@ -6,4 +6,7 @@ interface IFactory { external pure returns (string memory appDataString, bytes32 appDataHash); + function createLBP(address personalCRC, address backingAsset, uint256 backingAssetAmount) + external + returns (address lbp); } From 3b78c6c720902ec8b14a2179d18a2671a8684130 Mon Sep 17 00:00:00 2001 From: roleengineer Date: Fri, 10 Jan 2025 11:17:41 +0100 Subject: [PATCH 09/24] moved join pool call from factory to instance --- src/CirclesBacking.sol | 14 ++- src/factory/CirclesBackingFactory.sol | 26 +++--- src/interfaces/IFactory.sol | 4 +- test/CirclesBackingFactory.t.sol | 125 ++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 17 deletions(-) create mode 100644 test/CirclesBackingFactory.t.sol diff --git a/src/CirclesBacking.sol b/src/CirclesBacking.sol index 2697669..e3e612e 100644 --- a/src/CirclesBacking.sol +++ b/src/CirclesBacking.sol @@ -5,6 +5,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ICowswapSettlement} from "src/interfaces/ICowswapSettlement.sol"; import {IFactory} from "src/interfaces/IFactory.sol"; // temporary solution import {ILBP} from "src/interfaces/ILBP.sol"; +import {IVault} from "src/interfaces/IVault.sol"; contract CirclesBacking { /// Already initialized. @@ -81,11 +82,22 @@ contract CirclesBacking { if (backingAssetBalance == 0) revert InsufficientBackingAssetBalance(); // Create LBP + bytes32 poolId; + IVault.JoinPoolRequest memory request; + + (lbp, poolId, request) = FACTORY.createLBP(personalCircles, backingAsset, backingAssetBalance); + // approve vault IERC20(personalCircles).approve(VAULT_BALANCER, backingAssetBalance); IERC20(backingAsset).approve(VAULT_BALANCER, CRC_AMOUNT); - lbp = FACTORY.createLBP(personalCircles, backingAsset, backingAssetBalance); + // provide liquidity into lbp + IVault(VAULT_BALANCER).joinPool( + poolId, + address(this), // sender + address(this), // recipient + request + ); // update weight gradually uint256 timestampInYear = block.timestamp + UPDATE_WEIGHT_DURATION; diff --git a/src/factory/CirclesBackingFactory.sol b/src/factory/CirclesBackingFactory.sol index ac1ffe8..0578f2b 100644 --- a/src/factory/CirclesBackingFactory.sol +++ b/src/factory/CirclesBackingFactory.sol @@ -36,13 +36,14 @@ contract CirclesBackingFactory { event CirclesBackingDeployed(address indexed backer, address indexed circlesBackingInstance); /// @notice Emitted when a LBP is created. event LBPCreated(address indexed circlesBackingInstance, address indexed lbp); - event CirclesBackingCompleted( + + event CirclesBackingInitiated( address indexed backer, - address indexed backingAsset, address indexed circlesBackingInstance, - address lbp, - address personalCRC + address backingAsset, + address personalCirclesAddress ); + event CirclesBackingCompleted(address indexed backer, address indexed circlesBackingInstance, address lbp); // Cowswap order constants. /// @notice Helper contract for crafting Uid. @@ -144,6 +145,7 @@ contract CirclesBackingFactory { CirclesBacking(instance).initiateBacking( msg.sender, backingAsset, personalCirclesAddress, orderUid, USDC, TRADE_AMOUNT ); + emit CirclesBackingInitiated(msg.sender, instance, backingAsset, personalCirclesAddress); } // personal circles @@ -229,7 +231,7 @@ contract CirclesBackingFactory { /// @param personalCRC . function createLBP(address personalCRC, address backingAsset, uint256 backingAssetAmount) external - returns (address lbp) + returns (address lbp, bytes32 poolId, IVault.JoinPoolRequest memory request) { address backer = backerOf[msg.sender]; if (backer == address(0)) revert OnlyCirclesBacking(); @@ -257,23 +259,17 @@ contract CirclesBackingFactory { emit LBPCreated(backer, lbp); - bytes32 poolId = ILBP(lbp).getPoolId(); + poolId = ILBP(lbp).getPoolId(); uint256[] memory amountsIn = new uint256[](2); amountsIn[0] = tokenZero ? CRC_AMOUNT : backingAssetAmount; amountsIn[1] = tokenZero ? backingAssetAmount : CRC_AMOUNT; bytes memory userData = abi.encode(ILBP.JoinKind.INIT, amountsIn); - // CHECK: only owner can join pool, however it looks like anyone can do this call setting owner address as sender - // provide liquidity into lbp - IVault(VAULT).joinPool( - poolId, - msg.sender, // sender - msg.sender, // recipient - IVault.JoinPoolRequest(tokens, amountsIn, userData, false) - ); - emit CirclesBackingCompleted(backer, backingAsset, msg.sender, lbp, personalCRC); + request = IVault.JoinPoolRequest(tokens, amountsIn, userData, false); + + emit CirclesBackingCompleted(backer, msg.sender, lbp); } /// @notice General wrapper function over vault.exitPool, allows to extract diff --git a/src/interfaces/IFactory.sol b/src/interfaces/IFactory.sol index e1d6a17..f6c8cd3 100644 --- a/src/interfaces/IFactory.sol +++ b/src/interfaces/IFactory.sol @@ -1,6 +1,8 @@ // 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 @@ -8,5 +10,5 @@ interface IFactory { returns (string memory appDataString, bytes32 appDataHash); function createLBP(address personalCRC, address backingAsset, uint256 backingAssetAmount) external - returns (address lbp); + returns (address lbp, bytes32 poolId, IVault.JoinPoolRequest memory request); } diff --git a/test/CirclesBackingFactory.t.sol b/test/CirclesBackingFactory.t.sol new file mode 100644 index 0000000..d63630a --- /dev/null +++ b/test/CirclesBackingFactory.t.sol @@ -0,0 +1,125 @@ +// 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 {CirclesBackingFactory} from "src/factory/CirclesBackingFactory.sol"; +import {IVault} from "src/interfaces/IVault.sol"; +import {INoProtocolFeeLiquidityBootstrappingPoolFactory} from "src/interfaces/ILBPFactory.sol"; +import {ILBP} from "src/interfaces/ILBP.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; + +contract MockToken is ERC20 { + /** + * @notice Constructor - solmate ERC20 token. + * @param _name Token name. + * @param _symbol Token symbol. + * @param _totalSupply Token total supply. + */ + constructor(string memory _name, string memory _symbol, uint256 _totalSupply) + payable + ERC20(_name, _symbol, uint8(18)) + { + _mint(msg.sender, _totalSupply); + } +} + +contract MockJoin { + /// @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; + /// @notice Amount of InflationaryCircles to use in LBP initial liquidity. + uint256 public constant CRC_AMOUNT = 48 ether; + + /// @notice Emitted when a LBP is created. + event LBPCreated(address indexed circlesBackingInstance, address indexed lbp); + + /// @notice Creates LBP with underlying assets: `backingAssetAmount` backingAsset(`backingAsset`) and `CRC_AMOUNT` InflationaryCircles(`personalCRC`). + /// @param personalCRC . + function createLBP(address personalCRC, address backingAsset, uint256 backingAssetAmount) + external + returns (address lbp) + { + // 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( + "sdf_name", + "sdf_symbol", + tokens, + weights, + SWAP_FEE, + msg.sender, // lbp owner + true // enable swap on start + ); + + emit LBPCreated(msg.sender, lbp); + + bytes32 poolId = ILBP(lbp).getPoolId(); + + uint256[] memory amountsIn = new uint256[](2); + amountsIn[0] = tokenZero ? CRC_AMOUNT : backingAssetAmount; + amountsIn[1] = tokenZero ? backingAssetAmount : CRC_AMOUNT; + + bytes memory userData = abi.encode(ILBP.JoinKind.INIT, amountsIn); + // CHECK: only owner can join pool, however it looks like anyone can do this call setting owner address as sender + // provide liquidity into lbp + IVault(VAULT).joinPool( + poolId, + msg.sender, // sender + msg.sender, // recipient + IVault.JoinPoolRequest(tokens, amountsIn, userData, false) + ); + } +} + +contract CirclesBackingFactoryTest is Test { + CirclesBackingFactory public factory; + MockJoin public mockJoin; + address testAccount = address(0x458437598234234234); + address personalCRC; + address backingAsset; + address VAULT; + uint256 backingAssetAmount = 100e6; + + uint256 blockNumber = 37968717; + uint256 gnosis; + + function setUp() public { + gnosis = vm.createFork(vm.envString("GNOSIS_RPC"), blockNumber); + vm.selectFork(gnosis); + factory = new CirclesBackingFactory(); + mockJoin = new MockJoin(); + personalCRC = address(new MockToken("crc", "crc", 10_000 ether)); + IERC20(personalCRC).transfer(testAccount, 10_000 ether); + backingAsset = address(0x2a22f9c3b484c3629090FeED35F17Ff8F88f76F0); // usdc + deal(backingAsset, testAccount, 1000e6); + VAULT = mockJoin.VAULT(); + } + + function test_Join() public { + vm.prank(testAccount); + IERC20(personalCRC).approve(VAULT, 48 ether); + vm.prank(testAccount); + IERC20(backingAsset).approve(VAULT, backingAssetAmount); + + mockJoin.createLBP(personalCRC, backingAsset, backingAssetAmount); + } +} From bdafd67bce8e7730570d62b48e1dc658c8db22ad Mon Sep 17 00:00:00 2001 From: roleengineer Date: Fri, 10 Jan 2025 11:47:47 +0100 Subject: [PATCH 10/24] implemented claim bpt --- src/CirclesBacking.sol | 22 ++++++++++------------ src/factory/CirclesBackingFactory.sol | 1 + src/interfaces/IFactory.sol | 1 + 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/CirclesBacking.sol b/src/CirclesBacking.sol index e3e612e..b69b215 100644 --- a/src/CirclesBacking.sol +++ b/src/CirclesBacking.sol @@ -12,6 +12,8 @@ contract CirclesBacking { 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. @@ -76,6 +78,7 @@ contract CirclesBacking { // 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)); @@ -84,7 +87,6 @@ contract CirclesBacking { // Create LBP bytes32 poolId; IVault.JoinPoolRequest memory request; - (lbp, poolId, request) = FACTORY.createLBP(personalCircles, backingAsset, backingAssetBalance); // approve vault @@ -105,23 +107,19 @@ contract CirclesBacking { // set bpt unlock balancerPoolTokensUnlockTimestamp = timestampInYear; - - // need to lock, so only 1 call } function claimBalancerPoolTokens() external { if (msg.sender != backer) revert NotBacker(); - /* - if () - if (unlockTimestamp == 0) revert NotAUser(); - if (unlockTimestamp > block.timestamp) revert TokensLockedUntilTimestamp(unlockTimestamp); - userToLBPData[msg.sender].bptUnlockTimestamp = 0; + if (!FACTORY.releaseAvailable()) { + if (balancerPoolTokensUnlockTimestamp > block.timestamp) { + revert TokensLockedUntilTimestamp(balancerPoolTokensUnlockTimestamp); + } + } - IERC20 lbp = IERC20(userToLBPData[msg.sender].lbp); - uint256 bptAmount = lbp.balanceOf(address(this)); - lbp.transfer(msg.sender, bptAmount); - */ + uint256 bptAmount = IERC20(lbp).balanceOf(address(this)); + IERC20(lbp).transfer(msg.sender, bptAmount); } function _endWeights() internal pure returns (uint256[] memory endWeights) { diff --git a/src/factory/CirclesBackingFactory.sol b/src/factory/CirclesBackingFactory.sol index 0578f2b..3e08ba3 100644 --- a/src/factory/CirclesBackingFactory.sol +++ b/src/factory/CirclesBackingFactory.sol @@ -86,6 +86,7 @@ contract CirclesBackingFactory { mapping(address supportedAsset => bool) public supportedBackingAssets; mapping(address circleBacking => address backer) public backerOf; + bool public releaseAvailable; constructor() { VALID_TO = uint32(block.timestamp + 1825 days); diff --git a/src/interfaces/IFactory.sol b/src/interfaces/IFactory.sol index f6c8cd3..1c9f33e 100644 --- a/src/interfaces/IFactory.sol +++ b/src/interfaces/IFactory.sol @@ -11,4 +11,5 @@ interface IFactory { function createLBP(address personalCRC, address backingAsset, uint256 backingAssetAmount) external returns (address lbp, bytes32 poolId, IVault.JoinPoolRequest memory request); + function releaseAvailable() external view returns (bool); } From ef3d20832b21591722d3865be71e855c63b3a644 Mon Sep 17 00:00:00 2001 From: roleengineer Date: Fri, 10 Jan 2025 12:20:41 +0100 Subject: [PATCH 11/24] added factory admin functions --- script/DeployFactory.s.sol | 2 +- src/CirclesBacking.sol | 2 +- src/factory/CirclesBackingFactory.sol | 32 ++++++++++++++++++--------- src/interfaces/IFactory.sol | 2 +- test/CirclesBackingFactory.t.sol | 3 ++- 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/script/DeployFactory.s.sol b/script/DeployFactory.s.sol index b8e575b..bcf2d0e 100644 --- a/script/DeployFactory.s.sol +++ b/script/DeployFactory.s.sol @@ -13,7 +13,7 @@ contract DeployFactory is Script { function run() public { vm.startBroadcast(deployer); - circlesBackingFactory = new CirclesBackingFactory(); + circlesBackingFactory = new CirclesBackingFactory(deployer); vm.stopBroadcast(); console.log(address(circlesBackingFactory), "CirclesBackingFactory"); diff --git a/src/CirclesBacking.sol b/src/CirclesBacking.sol index b69b215..a6022ec 100644 --- a/src/CirclesBacking.sol +++ b/src/CirclesBacking.sol @@ -112,7 +112,7 @@ contract CirclesBacking { function claimBalancerPoolTokens() external { if (msg.sender != backer) revert NotBacker(); - if (!FACTORY.releaseAvailable()) { + if (FACTORY.releaseTimestamp() > uint32(block.timestamp)) { if (balancerPoolTokensUnlockTimestamp > block.timestamp) { revert TokensLockedUntilTimestamp(balancerPoolTokensUnlockTimestamp); } diff --git a/src/factory/CirclesBackingFactory.sol b/src/factory/CirclesBackingFactory.sol index 3e08ba3..af1360b 100644 --- a/src/factory/CirclesBackingFactory.sol +++ b/src/factory/CirclesBackingFactory.sol @@ -14,7 +14,7 @@ import {CirclesBacking} from "src/CirclesBacking.sol"; /** * @title Circles Backing Factory. * @notice Contract allows to create CircleBacking instances. - * Factory should have an admin function to make release of lbp for everyone. + * Administrates supported backing assets and global balancer pool tokens release. */ contract CirclesBackingFactory { /// Circles backing does not support `requestedAsset` asset. @@ -25,10 +25,8 @@ contract CirclesBackingFactory { error PersonalCirclesApprovalIsMissing(); /// Method can be called only by instance of CirclesBacking deployed by this factory. error OnlyCirclesBacking(); - /// Method requires exact `requiredXDai` xDai amount, was provided: `providedXDai`. - error NotExactXDaiAmount(uint256 providedXDai, uint256 requiredXDai); - /// LBP was created previously, currently only 1 LBP per user can be created. - error OnlyOneLBPPerUser(); + /// Unauthorized access. + error NotAdmin(); /// Exit Liquidity Bootstraping Pool supports only two tokens pools. error OnlyTwoTokenLBPSupported(); @@ -83,12 +81,20 @@ contract CirclesBackingFactory { 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; + /// @notice Address allowed to set supported backing assets and global bpt release timestamp. + address public immutable ADMIN; mapping(address supportedAsset => bool) public supportedBackingAssets; mapping(address circleBacking => address backer) public backerOf; - bool public releaseAvailable; + uint32 public releaseTimestamp = type(uint32).max; - constructor() { + modifier onlyAdmin() { + if (msg.sender != ADMIN) revert NotAdmin(); + _; + } + + constructor(address admin) { + ADMIN = admin; VALID_TO = uint32(block.timestamp + 1825 days); supportedBackingAssets[address(0x8e5bBbb09Ed1ebdE8674Cda39A0c169401db4252)] = true; // WBTC supportedBackingAssets[address(0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1)] = true; // WETH @@ -223,9 +229,6 @@ contract CirclesBackingFactory { ); } - // admin logic - // TODO - // LBP logic /// @notice Creates LBP with underlying assets: `backingAssetAmount` backingAsset(`backingAsset`) and `CRC_AMOUNT` InflationaryCircles(`personalCRC`). @@ -273,6 +276,15 @@ contract CirclesBackingFactory { emit CirclesBackingCompleted(backer, msg.sender, lbp); } + // ADMIN logic + function setReleaseTimestamp(uint32 timestamp) external onlyAdmin { + releaseTimestamp = timestamp; + } + + function setSupportedBackingAssetStatus(address backingAsset, bool status) external onlyAdmin { + supportedBackingAssets[backingAsset] = status; + } + /// @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 diff --git a/src/interfaces/IFactory.sol b/src/interfaces/IFactory.sol index 1c9f33e..ac18e5f 100644 --- a/src/interfaces/IFactory.sol +++ b/src/interfaces/IFactory.sol @@ -11,5 +11,5 @@ interface IFactory { function createLBP(address personalCRC, address backingAsset, uint256 backingAssetAmount) external returns (address lbp, bytes32 poolId, IVault.JoinPoolRequest memory request); - function releaseAvailable() external view returns (bool); + function releaseTimestamp() external view returns (uint32); } diff --git a/test/CirclesBackingFactory.t.sol b/test/CirclesBackingFactory.t.sol index d63630a..2570d6e 100644 --- a/test/CirclesBackingFactory.t.sol +++ b/test/CirclesBackingFactory.t.sol @@ -92,6 +92,7 @@ contract MockJoin { contract CirclesBackingFactoryTest is Test { CirclesBackingFactory public factory; + address factoryAdmin = address(0x4583759874359754305480345); MockJoin public mockJoin; address testAccount = address(0x458437598234234234); address personalCRC; @@ -105,7 +106,7 @@ contract CirclesBackingFactoryTest is Test { function setUp() public { gnosis = vm.createFork(vm.envString("GNOSIS_RPC"), blockNumber); vm.selectFork(gnosis); - factory = new CirclesBackingFactory(); + factory = new CirclesBackingFactory(factoryAdmin); mockJoin = new MockJoin(); personalCRC = address(new MockToken("crc", "crc", 10_000 ether)); IERC20(personalCRC).transfer(testAccount, 10_000 ether); From c2f930c2ac4acd8f931a8c749a8da6da69a59a47 Mon Sep 17 00:00:00 2001 From: roleengineer Date: Fri, 10 Jan 2025 13:56:28 +0100 Subject: [PATCH 12/24] prettified factory --- src/factory/CirclesBackingFactory.sol | 201 ++++++++++++++------------ 1 file changed, 112 insertions(+), 89 deletions(-) diff --git a/src/factory/CirclesBackingFactory.sol b/src/factory/CirclesBackingFactory.sol index af1360b..a98eace 100644 --- a/src/factory/CirclesBackingFactory.sol +++ b/src/factory/CirclesBackingFactory.sol @@ -30,19 +30,25 @@ contract CirclesBackingFactory { /// 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 LBPCreated(address indexed circlesBackingInstance, address indexed lbp); - + 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)); @@ -81,18 +87,22 @@ contract CirclesBackingFactory { 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; - /// @notice Address allowed to set supported backing assets and global bpt release timestamp. - address public immutable ADMIN; + // Storage + /// @notice Stores supported assets. mapping(address supportedAsset => bool) public supportedBackingAssets; + /// @notice Links CirclesBacking instances to their creators. mapping(address circleBacking => address backer) public backerOf; + /// @notice Global release timestamp for balancer pool tokens. uint32 public releaseTimestamp = type(uint32).max; + // Modifier modifier onlyAdmin() { if (msg.sender != ADMIN) revert NotAdmin(); _; } + // Constructor constructor(address admin) { ADMIN = admin; VALID_TO = uint32(block.timestamp + 1825 days); @@ -103,6 +113,20 @@ contract CirclesBackingFactory { supportedBackingAssets[USDC] = true; // USDC } + // 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 CRC and USDC.e function startBacking(address backingAsset) external { if (!supportedBackingAssets[backingAsset]) revert UnsupportedBackingAsset(backingAsset); @@ -155,84 +179,12 @@ contract CirclesBackingFactory { emit CirclesBackingInitiated(msg.sender, instance, backingAsset, personalCirclesAddress); } - // personal circles - - // @dev this call will revert, if avatar is not registered as human or group in Hub contract - function getPersonalCircles(address avatar) public view returns (address inflationaryCircles) { - inflationaryCircles = LIFT_ERC20.erc20Circles(uint8(1), avatar); - // TODO: find capacity to understand why i had this revert - //if (inflationaryCircles == address(0)) revert InflationaryCirclesNotExists(avatar); - } - - // cowswap app data - - 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)); - } - - 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); - } - - // 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) { - // open question: do we want backer to be able to create only one backing? - this is how it is now. - // or we allow backer to create multiple backings, 1 per supported backing asset - need to add backing asset to salt. - 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; - - emit CirclesBackingDeployed(backer, deployedAddress); - } - - // 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))))) - ); - } - // LBP logic /// @notice Creates LBP with underlying assets: `backingAssetAmount` backingAsset(`backingAsset`) and `CRC_AMOUNT` InflationaryCircles(`personalCRC`). - /// @param 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, address backingAsset, uint256 backingAssetAmount) external returns (address lbp, bytes32 poolId, IVault.JoinPoolRequest memory request) @@ -261,7 +213,7 @@ contract CirclesBackingFactory { true // enable swap on start ); - emit LBPCreated(backer, lbp); + emit LBPDeployed(msg.sender, lbp); poolId = ILBP(lbp).getPoolId(); @@ -276,15 +228,6 @@ contract CirclesBackingFactory { emit CirclesBackingCompleted(backer, msg.sender, lbp); } - // ADMIN logic - function setReleaseTimestamp(uint32 timestamp) external onlyAdmin { - releaseTimestamp = timestamp; - } - - function setSupportedBackingAssetStatus(address backingAsset, bool status) external onlyAdmin { - supportedBackingAssets[backingAsset] = status; - } - /// @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 @@ -312,12 +255,92 @@ contract CirclesBackingFactory { ); } + // 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 view returns (address inflationaryCircles) { + inflationaryCircles = LIFT_ERC20.erc20Circles(uint8(1), avatar); + // TODO: find capacity to understand why i had this revert + //if (inflationaryCircles == address(0)) revert InflationaryCirclesNotExists(avatar); + } + // 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) { + // open question: do we want backer to be able to create only one backing? - this is how it is now. + // or we allow backer to create multiple backings, 1 per supported backing asset - need to add backing asset to salt. + 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; + + 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())); } From 2a55831e03a492daa0df557d3e95c638fa1e75d7 Mon Sep 17 00:00:00 2001 From: roleengineer Date: Fri, 10 Jan 2025 14:33:00 +0100 Subject: [PATCH 13/24] prettified instance --- src/CirclesBacking.sol | 49 ++++++++++++++++++++------- src/factory/CirclesBackingFactory.sol | 12 ++++++- src/interfaces/IFactory.sol | 8 ++++- 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/CirclesBacking.sol b/src/CirclesBacking.sol index a6022ec..e6db56f 100644 --- a/src/CirclesBacking.sol +++ b/src/CirclesBacking.sol @@ -3,11 +3,12 @@ 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"; // temporary solution +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. @@ -18,35 +19,47 @@ contract CirclesBacking { error InsufficientBackingAssetBalance(); /// Unauthorized access. error NotBacker(); - /// Balancer Pool Tokens are still locked. + /// 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; - address public constant VAULT_BALANCER = address(0xBA12222222228d8Ba445958a75a0704d566BF2C8); + /// @dev Circles Backing Factory. IFactory internal immutable FACTORY; - /// @notice Amount of InflationaryCircles to use in LBP initial liquidity. - uint256 public constant CRC_AMOUNT = 48 ether; /// @dev LBP token weight 50%. uint256 internal constant WEIGHT_50 = 0.5 ether; - /// @dev Update weight duration is set to 1 year. + /// @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 (InlationaryCircles), 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; + /// @notice Timestamp, when locked balancer pool tokens are allowed to be claimed by backer. uint256 public balancerPoolTokensUnlockTimestamp; - + /// @notice Cowswap order uid. bytes public storedOrderUid; - event OrderCreated(bytes orderUid); - 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, @@ -74,6 +87,8 @@ contract CirclesBacking { 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); @@ -87,14 +102,17 @@ contract CirclesBacking { // Create LBP bytes32 poolId; IVault.JoinPoolRequest memory request; - (lbp, poolId, request) = FACTORY.createLBP(personalCircles, backingAsset, backingAssetBalance); + address vault; + uint256 circlesAmount; + (lbp, poolId, request, vault, circlesAmount) = + FACTORY.createLBP(personalCircles, backingAsset, backingAssetBalance); // approve vault - IERC20(personalCircles).approve(VAULT_BALANCER, backingAssetBalance); - IERC20(backingAsset).approve(VAULT_BALANCER, CRC_AMOUNT); + IERC20(personalCircles).approve(vault, backingAssetBalance); + IERC20(backingAsset).approve(vault, circlesAmount); // provide liquidity into lbp - IVault(VAULT_BALANCER).joinPool( + IVault(vault).joinPool( poolId, address(this), // sender address(this), // recipient @@ -109,6 +127,9 @@ contract CirclesBacking { balancerPoolTokensUnlockTimestamp = timestampInYear; } + // Balancer pool tokens + + /// @notice Method allows backer to claim balancer pool tokens after lock period or in case of global release. function claimBalancerPoolTokens() external { if (msg.sender != backer) revert NotBacker(); @@ -122,6 +143,8 @@ contract CirclesBacking { IERC20(lbp).transfer(msg.sender, bptAmount); } + // Internal functions + function _endWeights() internal pure returns (uint256[] memory endWeights) { endWeights = new uint256[](2); endWeights[0] = WEIGHT_50; diff --git a/src/factory/CirclesBackingFactory.sol b/src/factory/CirclesBackingFactory.sol index a98eace..123fefa 100644 --- a/src/factory/CirclesBackingFactory.sol +++ b/src/factory/CirclesBackingFactory.sol @@ -17,6 +17,7 @@ import {CirclesBacking} from "src/CirclesBacking.sol"; * Administrates supported backing assets and global balancer pool tokens release. */ contract CirclesBackingFactory { + // Errors /// Circles backing does not support `requestedAsset` asset. error UnsupportedBackingAsset(address requestedAsset); /// Deployment of CirclesBacking instance initiated by user `backer` has failed. @@ -187,11 +188,20 @@ contract CirclesBackingFactory { /// @param backingAssetAmount Amount of backing asset used in lbp. function createLBP(address personalCRC, address backingAsset, uint256 backingAssetAmount) external - returns (address lbp, bytes32 poolId, IVault.JoinPoolRequest memory request) + returns ( + address lbp, + bytes32 poolId, + IVault.JoinPoolRequest memory request, + address vault, + uint256 circlesAmount + ) { address backer = backerOf[msg.sender]; if (backer == address(0)) revert OnlyCirclesBacking(); + vault = VAULT; + circlesAmount = CRC_AMOUNT; // naturally to use stack value further as we have to return it anyway, but need to check what is cheaper constant or stack. + // prepare inputs IERC20[] memory tokens = new IERC20[](2); bool tokenZero = personalCRC < backingAsset; diff --git a/src/interfaces/IFactory.sol b/src/interfaces/IFactory.sol index ac18e5f..81c5356 100644 --- a/src/interfaces/IFactory.sol +++ b/src/interfaces/IFactory.sol @@ -10,6 +10,12 @@ interface IFactory { returns (string memory appDataString, bytes32 appDataHash); function createLBP(address personalCRC, address backingAsset, uint256 backingAssetAmount) external - returns (address lbp, bytes32 poolId, IVault.JoinPoolRequest memory request); + returns ( + address lbp, + bytes32 poolId, + IVault.JoinPoolRequest memory request, + address vault, + uint256 circlesAmount + ); function releaseTimestamp() external view returns (uint32); } From c1e7dcfbb0f2eca19c0074d153e95776a2573a4c Mon Sep 17 00:00:00 2001 From: roleengineer Date: Fri, 10 Jan 2025 16:40:37 +0100 Subject: [PATCH 14/24] adjusted metri flow batch tx: approve usdc, make 48crc erc1155 transferFrom to factory --- src/CirclesBacking.sol | 14 +++-- src/factory/CirclesBackingFactory.sol | 87 +++++++++++++++------------ src/interfaces/IFactory.sol | 10 +-- src/interfaces/IHub.sol | 1 + 4 files changed, 59 insertions(+), 53 deletions(-) diff --git a/src/CirclesBacking.sol b/src/CirclesBacking.sol index e6db56f..d9d16fb 100644 --- a/src/CirclesBacking.sol +++ b/src/CirclesBacking.sol @@ -48,6 +48,7 @@ contract CirclesBacking { 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. @@ -66,13 +67,15 @@ contract CirclesBacking { address _personalCircles, bytes memory orderUid, address usdc, - uint256 tradeAmount + 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); @@ -103,13 +106,12 @@ contract CirclesBacking { bytes32 poolId; IVault.JoinPoolRequest memory request; address vault; - uint256 circlesAmount; - (lbp, poolId, request, vault, circlesAmount) = - FACTORY.createLBP(personalCircles, backingAsset, backingAssetBalance); + (lbp, poolId, request, vault) = + FACTORY.createLBP(personalCircles, stableCirclesAmount, backingAsset, backingAssetBalance); // approve vault - IERC20(personalCircles).approve(vault, backingAssetBalance); - IERC20(backingAsset).approve(vault, circlesAmount); + IERC20(personalCircles).approve(vault, stableCirclesAmount); + IERC20(backingAsset).approve(vault, backingAssetBalance); // provide liquidity into lbp IVault(vault).joinPool( diff --git a/src/factory/CirclesBackingFactory.sol b/src/factory/CirclesBackingFactory.sol index 123fefa..5101eb7 100644 --- a/src/factory/CirclesBackingFactory.sol +++ b/src/factory/CirclesBackingFactory.sol @@ -18,6 +18,12 @@ import {CirclesBacking} from "src/CirclesBacking.sol"; */ 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 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. @@ -128,33 +134,19 @@ contract CirclesBackingFactory { // Backing logic - // @dev Required upfront approval of this contract for CRC and USDC.e - function startBacking(address backingAsset) external { + /// @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(msg.sender); - - address personalCirclesAddress = getPersonalCircles(msg.sender); - // handling personal CRC: 1. try to get Inflationary, if fails 2. try to get 1155 and wrap, if fails revert - try IERC20(personalCirclesAddress).transferFrom(msg.sender, instance, CRC_AMOUNT) {} - catch { - try HUB_V2.safeTransferFrom(msg.sender, address(this), uint256(uint160(msg.sender)), CRC_AMOUNT, "") { - // NOTE: for now this flow always reverts as not fully implemented - // Reason why not implemented except lack of time is that we might make startBacking internal function called inside - // IERC1155Receiver.onERC1155Received and the whole handling personal CRC flow will be refactored. - // TODO: - // 0. implement IERC1155Receiver.onERC1155Received here - // 1. define the exact erc1155 circles amount to get based on constant of erc20 inflationary constant - // 2. call wrap on HUB_V2 with the exact erc1155 circles amount and type = 1 (infationary) - // 3. check erc20 inflationary balance of address(this) equal CRC_AMOUNT - // 4. transfer to instance - } catch { - revert PersonalCirclesApprovalIsMissing(); - } - } + address instance = deployCirclesBacking(backer); - // handling USDC.e - IERC20(USDC).transferFrom(msg.sender, instance, TRADE_AMOUNT); + // transfer USDC.e + IERC20(USDC).transferFrom(backer, instance, TRADE_AMOUNT); + // transfer stable circles + IERC20(stableCRCAddress).transfer(instance, stableCRCAmount); // create order (, bytes32 appData) = getAppData(instance); @@ -175,9 +167,9 @@ contract CirclesBackingFactory { bytes memory orderUid = abi.encodePacked(orderDigest, instance, uint32(VALID_TO)); // Initiate backing CirclesBacking(instance).initiateBacking( - msg.sender, backingAsset, personalCirclesAddress, orderUid, USDC, TRADE_AMOUNT + backer, backingAsset, stableCRCAddress, orderUid, USDC, TRADE_AMOUNT, stableCRCAmount ); - emit CirclesBackingInitiated(msg.sender, instance, backingAsset, personalCirclesAddress); + emit CirclesBackingInitiated(backer, instance, backingAsset, stableCRCAddress); } // LBP logic @@ -186,22 +178,13 @@ contract CirclesBackingFactory { /// @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, address backingAsset, uint256 backingAssetAmount) + function createLBP(address personalCRC, uint256 personalCRCAmount, address backingAsset, uint256 backingAssetAmount) external - returns ( - address lbp, - bytes32 poolId, - IVault.JoinPoolRequest memory request, - address vault, - uint256 circlesAmount - ) + returns (address lbp, bytes32 poolId, IVault.JoinPoolRequest memory request, address vault) { address backer = backerOf[msg.sender]; if (backer == address(0)) revert OnlyCirclesBacking(); - vault = VAULT; - circlesAmount = CRC_AMOUNT; // naturally to use stack value further as we have to return it anyway, but need to check what is cheaper constant or stack. - // prepare inputs IERC20[] memory tokens = new IERC20[](2); bool tokenZero = personalCRC < backingAsset; @@ -228,12 +211,13 @@ contract CirclesBackingFactory { poolId = ILBP(lbp).getPoolId(); uint256[] memory amountsIn = new uint256[](2); - amountsIn[0] = tokenZero ? CRC_AMOUNT : backingAssetAmount; - amountsIn[1] = tokenZero ? backingAssetAmount : CRC_AMOUNT; + 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); } @@ -354,4 +338,29 @@ contract CirclesBackingFactory { 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 + 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 (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/interfaces/IFactory.sol b/src/interfaces/IFactory.sol index 81c5356..bae720b 100644 --- a/src/interfaces/IFactory.sol +++ b/src/interfaces/IFactory.sol @@ -8,14 +8,8 @@ interface IFactory { external pure returns (string memory appDataString, bytes32 appDataHash); - function createLBP(address personalCRC, address backingAsset, uint256 backingAssetAmount) + function createLBP(address personalCRC, uint256 personalCRCAmount, address backingAsset, uint256 backingAssetAmount) external - returns ( - address lbp, - bytes32 poolId, - IVault.JoinPoolRequest memory request, - address vault, - uint256 circlesAmount - ); + returns (address lbp, bytes32 poolId, IVault.JoinPoolRequest memory request, address vault); function releaseTimestamp() external view returns (uint32); } 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); } From 9a7ec4e061e8c275309363bede6f8b44dedc36c4 Mon Sep 17 00:00:00 2001 From: roleengineer Date: Fri, 10 Jan 2025 17:01:03 +0100 Subject: [PATCH 15/24] minor edits --- src/CirclesBacking.sol | 2 +- src/factory/CirclesBackingFactory.sol | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/CirclesBacking.sol b/src/CirclesBacking.sol index d9d16fb..508848f 100644 --- a/src/CirclesBacking.sol +++ b/src/CirclesBacking.sol @@ -132,7 +132,7 @@ contract CirclesBacking { // Balancer pool tokens /// @notice Method allows backer to claim balancer pool tokens after lock period or in case of global release. - function claimBalancerPoolTokens() external { + function releaseBalancerPoolTokens() external { if (msg.sender != backer) revert NotBacker(); if (FACTORY.releaseTimestamp() > uint32(block.timestamp)) { diff --git a/src/factory/CirclesBackingFactory.sol b/src/factory/CirclesBackingFactory.sol index 5101eb7..6514f50 100644 --- a/src/factory/CirclesBackingFactory.sol +++ b/src/factory/CirclesBackingFactory.sol @@ -117,7 +117,6 @@ contract CirclesBackingFactory { supportedBackingAssets[address(0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1)] = true; // WETH supportedBackingAssets[address(0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb)] = true; // GNO supportedBackingAssets[address(0xaf204776c7245bF4147c2612BF6e5972Ee483701)] = true; // sDAI - supportedBackingAssets[USDC] = true; // USDC } // Admin logic @@ -126,8 +125,8 @@ contract CirclesBackingFactory { function setReleaseTimestamp(uint32 timestamp) external onlyAdmin { releaseTimestamp = timestamp; } - /// @notice Method sets supported status for backing asset. + /// @notice Method sets supported status for backing asset. function setSupportedBackingAssetStatus(address backingAsset, bool status) external onlyAdmin { supportedBackingAssets[backingAsset] = status; } From d8f9e4dfc8721a8e86c83f5461da3b33f100b5d7 Mon Sep 17 00:00:00 2001 From: roleengineer Date: Fri, 10 Jan 2025 17:05:54 +0100 Subject: [PATCH 16/24] removed unused test --- test/CirclesBackingFactory.t.sol | 100 +------------------------------ 1 file changed, 3 insertions(+), 97 deletions(-) diff --git a/test/CirclesBackingFactory.t.sol b/test/CirclesBackingFactory.t.sol index 2570d6e..2381831 100644 --- a/test/CirclesBackingFactory.t.sol +++ b/test/CirclesBackingFactory.t.sol @@ -8,97 +8,15 @@ import {IVault} from "src/interfaces/IVault.sol"; import {INoProtocolFeeLiquidityBootstrappingPoolFactory} from "src/interfaces/ILBPFactory.sol"; import {ILBP} from "src/interfaces/ILBP.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; - -contract MockToken is ERC20 { - /** - * @notice Constructor - solmate ERC20 token. - * @param _name Token name. - * @param _symbol Token symbol. - * @param _totalSupply Token total supply. - */ - constructor(string memory _name, string memory _symbol, uint256 _totalSupply) - payable - ERC20(_name, _symbol, uint8(18)) - { - _mint(msg.sender, _totalSupply); - } -} - -contract MockJoin { - /// @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; - /// @notice Amount of InflationaryCircles to use in LBP initial liquidity. - uint256 public constant CRC_AMOUNT = 48 ether; - - /// @notice Emitted when a LBP is created. - event LBPCreated(address indexed circlesBackingInstance, address indexed lbp); - - /// @notice Creates LBP with underlying assets: `backingAssetAmount` backingAsset(`backingAsset`) and `CRC_AMOUNT` InflationaryCircles(`personalCRC`). - /// @param personalCRC . - function createLBP(address personalCRC, address backingAsset, uint256 backingAssetAmount) - external - returns (address lbp) - { - // 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( - "sdf_name", - "sdf_symbol", - tokens, - weights, - SWAP_FEE, - msg.sender, // lbp owner - true // enable swap on start - ); - - emit LBPCreated(msg.sender, lbp); - - bytes32 poolId = ILBP(lbp).getPoolId(); - - uint256[] memory amountsIn = new uint256[](2); - amountsIn[0] = tokenZero ? CRC_AMOUNT : backingAssetAmount; - amountsIn[1] = tokenZero ? backingAssetAmount : CRC_AMOUNT; - - bytes memory userData = abi.encode(ILBP.JoinKind.INIT, amountsIn); - // CHECK: only owner can join pool, however it looks like anyone can do this call setting owner address as sender - // provide liquidity into lbp - IVault(VAULT).joinPool( - poolId, - msg.sender, // sender - msg.sender, // recipient - IVault.JoinPoolRequest(tokens, amountsIn, userData, false) - ); - } -} contract CirclesBackingFactoryTest is Test { CirclesBackingFactory public factory; address factoryAdmin = address(0x4583759874359754305480345); - MockJoin public mockJoin; address testAccount = address(0x458437598234234234); address personalCRC; address backingAsset; address VAULT; - uint256 backingAssetAmount = 100e6; + uint256 usdcStartAmount = 100e6; uint256 blockNumber = 37968717; uint256 gnosis; @@ -107,20 +25,8 @@ contract CirclesBackingFactoryTest is Test { gnosis = vm.createFork(vm.envString("GNOSIS_RPC"), blockNumber); vm.selectFork(gnosis); factory = new CirclesBackingFactory(factoryAdmin); - mockJoin = new MockJoin(); - personalCRC = address(new MockToken("crc", "crc", 10_000 ether)); - IERC20(personalCRC).transfer(testAccount, 10_000 ether); - backingAsset = address(0x2a22f9c3b484c3629090FeED35F17Ff8F88f76F0); // usdc - deal(backingAsset, testAccount, 1000e6); - VAULT = mockJoin.VAULT(); + VAULT = factory.VAULT(); } - function test_Join() public { - vm.prank(testAccount); - IERC20(personalCRC).approve(VAULT, 48 ether); - vm.prank(testAccount); - IERC20(backingAsset).approve(VAULT, backingAssetAmount); - - mockJoin.createLBP(personalCRC, backingAsset, backingAssetAmount); - } + function test_Start() public {} } From cdfbf9f3acee2e1c7219307fb6feefc97d6d1669 Mon Sep 17 00:00:00 2001 From: roleengineer Date: Sat, 11 Jan 2025 19:01:42 +0100 Subject: [PATCH 17/24] resolve review comments: ensureERC20, only human avatars --- src/factory/CirclesBackingFactory.sol | 11 +++++------ src/interfaces/ILiftERC20.sol | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/factory/CirclesBackingFactory.sol b/src/factory/CirclesBackingFactory.sol index 6514f50..e56911f 100644 --- a/src/factory/CirclesBackingFactory.sol +++ b/src/factory/CirclesBackingFactory.sol @@ -22,6 +22,8 @@ contract CirclesBackingFactory { 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. @@ -279,10 +281,8 @@ contract CirclesBackingFactory { // 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 view returns (address inflationaryCircles) { - inflationaryCircles = LIFT_ERC20.erc20Circles(uint8(1), avatar); - // TODO: find capacity to understand why i had this revert - //if (inflationaryCircles == address(0)) revert InflationaryCirclesNotExists(avatar); + function getPersonalCircles(address avatar) public returns (address inflationaryCircles) { + inflationaryCircles = LIFT_ERC20.ensureERC20(avatar, uint8(1)); } // Internal functions @@ -294,8 +294,6 @@ contract CirclesBackingFactory { * @return deployedAddress Address of the deployed contract. */ function deployCirclesBacking(address backer) internal returns (address deployedAddress) { - // open question: do we want backer to be able to create only one backing? - this is how it is now. - // or we allow backer to create multiple backings, 1 per supported backing asset - need to add backing asset to salt. bytes32 salt_ = keccak256(abi.encodePacked(backer)); deployedAddress = address(new CirclesBacking{salt: salt_}()); @@ -346,6 +344,7 @@ contract CirclesBackingFactory { 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 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); } From f7d67bcf91549fa9d05b67f6990db21bf4c8fd28 Mon Sep 17 00:00:00 2001 From: Yevgeniy <35062472+roleengineer@users.noreply.github.com> Date: Sat, 11 Jan 2025 19:04:13 +0100 Subject: [PATCH 18/24] Update src/CirclesBacking.sol Co-authored-by: Benjamin Bollen --- src/CirclesBacking.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CirclesBacking.sol b/src/CirclesBacking.sol index 508848f..87f5768 100644 --- a/src/CirclesBacking.sol +++ b/src/CirclesBacking.sol @@ -44,7 +44,7 @@ contract CirclesBacking { 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 (InlationaryCircles), which is used as underlying asset in lbp. + /// @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; From b8bf3d14e886fb5c21d05bc762234509c24f4fa1 Mon Sep 17 00:00:00 2001 From: roleengineer Date: Sat, 11 Jan 2025 20:58:04 +0100 Subject: [PATCH 19/24] added reverse mapping backer-backing and view isActiveLBP --- src/CirclesBacking.sol | 2 ++ src/factory/CirclesBackingFactory.sol | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/CirclesBacking.sol b/src/CirclesBacking.sol index 508848f..cb2dac3 100644 --- a/src/CirclesBacking.sol +++ b/src/CirclesBacking.sol @@ -140,6 +140,8 @@ contract CirclesBacking { revert TokensLockedUntilTimestamp(balancerPoolTokensUnlockTimestamp); } } + // zeroed timestamp + balancerPoolTokensUnlockTimestamp = 0; uint256 bptAmount = IERC20(lbp).balanceOf(address(this)); IERC20(lbp).transfer(msg.sender, bptAmount); diff --git a/src/factory/CirclesBackingFactory.sol b/src/factory/CirclesBackingFactory.sol index e56911f..32c1595 100644 --- a/src/factory/CirclesBackingFactory.sol +++ b/src/factory/CirclesBackingFactory.sol @@ -101,7 +101,9 @@ contract CirclesBackingFactory { /// @notice Stores supported assets. mapping(address supportedAsset => bool) public supportedBackingAssets; /// @notice Links CirclesBacking instances to their creators. - mapping(address circleBacking => address backer) public backerOf; + 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; @@ -285,6 +287,13 @@ contract CirclesBackingFactory { inflationaryCircles = LIFT_ERC20.ensureERC20(avatar, uint8(1)); } + /// @notice Returns backer's LBP status. + function isLBPActive(address backer) external view returns (bool) { + address instance = circlesBackingOf[backer]; + uint256 unlockTimestamp = CirclesBacking(instance).balancerPoolTokensUnlockTimestamp(); + return unlockTimestamp > 0; + } + // Internal functions // deploy instance @@ -304,6 +313,8 @@ contract CirclesBackingFactory { // link instance to backer backerOf[deployedAddress] = backer; + // link backer to instance + circlesBackingOf[backer] = deployedAddress; emit CirclesBackingDeployed(backer, deployedAddress); } From 95b480d924d4cd75ef8eaf2fad266eed6589a3aa Mon Sep 17 00:00:00 2001 From: roleengineer Date: Sat, 11 Jan 2025 21:09:43 +0100 Subject: [PATCH 20/24] added receiver param in releaseBPT func --- src/CirclesBacking.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CirclesBacking.sol b/src/CirclesBacking.sol index 83c7bb0..b946be3 100644 --- a/src/CirclesBacking.sol +++ b/src/CirclesBacking.sol @@ -132,7 +132,8 @@ contract CirclesBacking { // Balancer pool tokens /// @notice Method allows backer to claim balancer pool tokens after lock period or in case of global release. - function releaseBalancerPoolTokens() external { + /// @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)) { @@ -144,7 +145,7 @@ contract CirclesBacking { balancerPoolTokensUnlockTimestamp = 0; uint256 bptAmount = IERC20(lbp).balanceOf(address(this)); - IERC20(lbp).transfer(msg.sender, bptAmount); + IERC20(lbp).transfer(receiver, bptAmount); } // Internal functions From e2036b43161de959089ca33e31b3ebe0feb74081 Mon Sep 17 00:00:00 2001 From: roleengineer Date: Sun, 12 Jan 2025 02:04:09 +0100 Subject: [PATCH 21/24] simulated backing flow to set gaslimit --- script/DeployFactory.s.sol | 2 +- src/factory/CirclesBackingFactory.sol | 29 +++++++++++++---- src/prototype/OrderCreator.sol | 2 +- test/CirclesBackingFactory.t.sol | 45 ++++++++++++++++++++++++--- 4 files changed, 65 insertions(+), 13 deletions(-) diff --git a/script/DeployFactory.s.sol b/script/DeployFactory.s.sol index bcf2d0e..8c8716c 100644 --- a/script/DeployFactory.s.sol +++ b/script/DeployFactory.s.sol @@ -13,7 +13,7 @@ contract DeployFactory is Script { function run() public { vm.startBroadcast(deployer); - circlesBackingFactory = new CirclesBackingFactory(deployer); + circlesBackingFactory = new CirclesBackingFactory(deployer, 1); vm.stopBroadcast(); console.log(address(circlesBackingFactory), "CirclesBackingFactory"); diff --git a/src/factory/CirclesBackingFactory.sol b/src/factory/CirclesBackingFactory.sol index 32c1595..d01aa7c 100644 --- a/src/factory/CirclesBackingFactory.sol +++ b/src/factory/CirclesBackingFactory.sol @@ -65,14 +65,14 @@ contract CirclesBackingFactory { 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 or for LBP initial liquidity in case USDC.e is backing asset. - uint256 public constant TRADE_AMOUNT = 100 * USDC_DECIMALS; + /// @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":"200000"}]}}}'; // Updated calldata for createLBP + string public constant postAppData = '","callData":"0x13e8f89f","gasLimit":"6000000"}]}}}'; // Updated calldata and gaslimit for createLBP /// LBP constants. /// @notice Balancer v2 Vault. @@ -107,15 +107,31 @@ contract CirclesBackingFactory { /// @notice Global release timestamp for balancer pool tokens. uint32 public releaseTimestamp = type(uint32).max; - // Modifier + // 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) { + 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 @@ -288,7 +304,7 @@ contract CirclesBackingFactory { } /// @notice Returns backer's LBP status. - function isLBPActive(address backer) external view returns (bool) { + function isActiveLBP(address backer) external view returns (bool) { address instance = circlesBackingOf[backer]; uint256 unlockTimestamp = CirclesBacking(instance).balancerPoolTokensUnlockTimestamp(); return unlockTimestamp > 0; @@ -350,6 +366,7 @@ contract CirclesBackingFactory { // 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(); diff --git a/src/prototype/OrderCreator.sol b/src/prototype/OrderCreator.sol index e5165d5..951eef8 100644 --- a/src/prototype/OrderCreator.sol +++ b/src/prototype/OrderCreator.sol @@ -32,7 +32,7 @@ contract OrderCreator { address public constant GNO = 0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb; address public constant GET_UID_CONTRACT = 0xCA51403B524dF7dA6f9D6BFc64895AD833b5d711; address public constant COWSWAP_SETTLEMENT_CONTRACT = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; - address public constant RECEIVER = 0x7B2e78D4dFaABA045A167a70dA285E30E8FcA196; + address public constant RECEIVER = 0x6BF173798733623cc6c221eD52c010472247d861; address public constant VAULT_RELAY = 0xC92E8bdf79f0507f65a392b0ab4667716BFE0110; uint256 public constant WXDAI_DECIMALS = 1e18; diff --git a/test/CirclesBackingFactory.t.sol b/test/CirclesBackingFactory.t.sol index 2381831..7392d0b 100644 --- a/test/CirclesBackingFactory.t.sol +++ b/test/CirclesBackingFactory.t.sol @@ -3,30 +3,65 @@ 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 {INoProtocolFeeLiquidityBootstrappingPoolFactory} from "src/interfaces/ILBPFactory.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(0x458437598234234234); + address testAccount = address(0x0865d14a4B688F24Bc8C282045A4A3cb9a26FbC2); + address WETH = address(0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1); address personalCRC; address backingAsset; address VAULT; + address USDC; uint256 usdcStartAmount = 100e6; + uint256 CRC_AMOUNT; - uint256 blockNumber = 37968717; + uint256 blockNumber = 37992624; uint256 gnosis; + bytes public uid; + function setUp() public { gnosis = vm.createFork(vm.envString("GNOSIS_RPC"), blockNumber); vm.selectFork(gnosis); - factory = new CirclesBackingFactory(factoryAdmin); + factory = new CirclesBackingFactory(factoryAdmin, uint256(100)); VAULT = factory.VAULT(); + USDC = factory.USDC(); + CRC_AMOUNT = factory.CRC_AMOUNT(); } - function test_Start() public {} + 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(); + } } From 1364bafc687f6cd892ec7b880046dbb1dabf898a Mon Sep 17 00:00:00 2001 From: roleengineer Date: Sun, 12 Jan 2025 04:29:15 +0100 Subject: [PATCH 22/24] deployed and refactored api call --- script/DeployFactory.s.sol | 26 +++++++++++++++++++++++++- script/DeployOrderCreator.s.sol | 24 ------------------------ 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/script/DeployFactory.s.sol b/script/DeployFactory.s.sol index 8c8716c..5ee6118 100644 --- a/script/DeployFactory.s.sol +++ b/script/DeployFactory.s.sol @@ -6,7 +6,7 @@ import {CirclesBackingFactory} from "src/factory/CirclesBackingFactory.sol"; contract DeployFactory is Script { address deployer = address(0x6BF173798733623cc6c221eD52c010472247d861); - CirclesBackingFactory public circlesBackingFactory; + CirclesBackingFactory public circlesBackingFactory; // 0xD608978aD1e1473fa98BaD368e767C5b11e3b3cE function setUp() public {} @@ -19,3 +19,27 @@ contract DeployFactory is Script { 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/DeployOrderCreator.s.sol b/script/DeployOrderCreator.s.sol index 9d30928..fcb1b5e 100644 --- a/script/DeployOrderCreator.s.sol +++ b/script/DeployOrderCreator.s.sol @@ -20,27 +20,3 @@ contract DeployPrototype is Script { console.log(address(orderCreator), "orderCreator"); } } - -/* -curl -X 'POST' \ - 'https://api.cow.fi/xdai/api/v1/orders' \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -d '{ - "sellToken": "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d", - "buyToken": "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb", - "receiver": "0xEA39b6F8F98f91ECCe24A5601FD02DF850d3eC3E", - "sellAmount": "100000000000000000", - "buyAmount": "1", - "validTo": 1894006860, - "feeAmount": "0", - "kind": "sell", - "partiallyFillable": false, - "sellTokenBalance": "erc20", - "buyTokenBalance": "erc20", - "signingScheme": "presign", - "signature": "0x", - "from": "0xEA39b6F8F98f91ECCe24A5601FD02DF850d3eC3E", - "appData": "{\"version\":\"1.1.0\",\"appCode\":\"Zeal powered by Qantura\",\"metadata\":{\"hooks\":{\"version\":\"0.1.0\",\"post\":[{\"target\":\"0xea39b6f8f98f91ecce24a5601fd02df850d3ec3e\",\"callData\":\"0xbb5ae136\",\"gasLimit\":\"200000\"}]}}}" -}' -*/ From cbca43a44c7151db16827e4ea957203f79f86ecb Mon Sep 17 00:00:00 2001 From: roleengineer Date: Sun, 12 Jan 2025 04:34:40 +0100 Subject: [PATCH 23/24] updated block --- test/CirclesBackingFactory.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/CirclesBackingFactory.t.sol b/test/CirclesBackingFactory.t.sol index 7392d0b..466dab3 100644 --- a/test/CirclesBackingFactory.t.sol +++ b/test/CirclesBackingFactory.t.sol @@ -24,7 +24,7 @@ contract CirclesBackingFactoryTest is Test { uint256 usdcStartAmount = 100e6; uint256 CRC_AMOUNT; - uint256 blockNumber = 37992624; + uint256 blockNumber = 37997665; uint256 gnosis; bytes public uid; From 4063931a5d21dbd43596f242f551c48512ff6a01 Mon Sep 17 00:00:00 2001 From: roleengineer Date: Sun, 12 Jan 2025 04:37:13 +0100 Subject: [PATCH 24/24] updated block again --- test/CirclesBackingFactory.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/CirclesBackingFactory.t.sol b/test/CirclesBackingFactory.t.sol index 466dab3..da32789 100644 --- a/test/CirclesBackingFactory.t.sol +++ b/test/CirclesBackingFactory.t.sol @@ -24,7 +24,7 @@ contract CirclesBackingFactoryTest is Test { uint256 usdcStartAmount = 100e6; uint256 CRC_AMOUNT; - uint256 blockNumber = 37997665; + uint256 blockNumber = 37997675; uint256 gnosis; bytes public uid;