Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
34c84db
WIP - buyDopplerCoinsWithCredits
sweetmantech Aug 11, 2025
44822c1
Unit Tests + setDopplerUniversalRouter - initial implementation
sweetmantech Aug 11, 2025
5053bcd
I update workflow to install universal router packages.
sweetmantech Aug 11, 2025
f6e8f68
I remove universal router lib.
sweetmantech Aug 11, 2025
550c59e
I remove changes to the github workflow.
sweetmantech Aug 11, 2025
1a9360b
I add unit test for test_RevertWhen_SetDopplerUniversalRouterWithNonC…
sweetmantech Aug 11, 2025
7456d12
add unit test to verify only owner can call with test_RevertWhen_NonO…
sweetmantech Aug 11, 2025
a8853d7
test_SetDopplerUniversalRouter - updated to test success state for se…
sweetmantech Aug 11, 2025
4012543
rollback changes to foundry.toml
sweetmantech Aug 11, 2025
9acda56
remove unused remappings
sweetmantech Aug 11, 2025
c31195c
Merge pull request #17 from Coop-Records/sweetmantech/coop-790-unit-t…
sweetmantech Aug 11, 2025
769faa5
Merge branch 'coins' of github.com:Coop-Records/CoopCreditsProtocol i…
sweetmantech Aug 11, 2025
5b41e5e
remove tokenAddress from function call.
sweetmantech Aug 11, 2025
32f3e80
add unit tests for revert reasons.
sweetmantech Aug 11, 2025
932feba
remove redundant param for ethAmount
sweetmantech Aug 11, 2025
e5706d2
Merge pull request #18 from Coop-Records/sweetmantech/coop-786-unit-t…
sweetmantech Aug 11, 2025
8966cb7
buyDopplerCoinsWithCredits - use credits and remove payable
sweetmantech Aug 11, 2025
a182804
refactor placement of modifier to bottom of file.
sweetmantech Aug 11, 2025
21c16e6
Merge pull request #19 from Coop-Records/sweetmantech/coop-792-buydop…
sweetmantech Aug 11, 2025
5d527ff
initializer - add param for dopplerUniversalRouter
sweetmantech Aug 11, 2025
152a427
add doppler router to deploy script
sweetmantech Aug 11, 2025
1489667
rollback changes to broadcast directory
sweetmantech Aug 11, 2025
fe58be2
fix failing tests by adding initial value for dopplerUniversalRouter.
sweetmantech Aug 11, 2025
fafe1c3
Merge pull request #20 from Coop-Records/sweetmantech/coop-793-initia…
sweetmantech Aug 11, 2025
cf0cb74
Unit Tests + buyWowCoinsWithCredits - initial implementation
sweetmantech Aug 14, 2025
0401e2b
onlyContracts - new modifier to DRY check for valid contract address.
sweetmantech Aug 14, 2025
351edae
test_RevertWhen_BuyCoopCoinsWithCreditsNonContractAddress - new unit …
sweetmantech Aug 14, 2025
98d46ee
formatting
sweetmantech Aug 14, 2025
d312a80
Merge pull request #21 from Coop-Records/sweetmantech/coop-787-unit-t…
sweetmantech Aug 14, 2025
df79975
Merge branch 'main' of github.com:Coop-Records/CoopCreditsProtocol in…
sweetmantech Aug 15, 2025
49e4412
Deploy script - setDopplerUniversalRouter
sweetmantech Aug 18, 2025
62f7ec9
rollback run-latest
sweetmantech Aug 18, 2025
97d1b9d
Merge pull request #24 from Coop-Records/sweetmantech/coop-806-deploy…
sweetmantech Aug 18, 2025
09c7f42
README - script instructions for Deploy and Upgrade pnpm + ENV
sweetmantech Aug 18, 2025
49b6a80
Merge pull request #25 from Coop-Records/sweetmantech/coop-807-readme…
sweetmantech Aug 18, 2025
38e3ec4
upgrade testnet proxy with new coins methods
sweetmantech Aug 18, 2025
c05f82b
Upgrade script execution on baseSepolia.
sweetmantech Aug 18, 2025
7a36f4c
Merge pull request #26 from Coop-Records/sweetmantech/coop-788-upgrad…
sweetmantech Aug 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
# Base Sepolia RPC URL
RPC_URL=https://sepolia.base.org

# Your deployment wallet's private key (generate with `npm run generate-wallet`)
# Example format: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
PRIVATE_KEY=

# Basescan API Key for contract verification
# Get one from https://basescan.org/apis
BASESCAN_API_KEY=

# RPC URL
# Base Sepolia: https://sepolia.base.org
# Base Mainnet: https://mainnet.base.org
RPC_URL=https://sepolia.base.org

# Fixed Price Sale Strategy address for initialization
# Base Sepolia: 0xd34872BE0cdb6b09d45FCa067B07f04a1A9aE1aE
# Base Mainnet: 0x04E2516A2c207E84a1839755675dfd8eF6302F0a
FIXED_PRICE_SALE_STRATEGY=

# IPFS URI for the token metadata
# Example format: ipfs://Qm...
TOKEN_URI=ipfs://
TOKEN_URI=ar://o3ni6dHT2v1fda8610XgjXx-3jGuBb4b--0pfbOxrmw

# Basescan API Key for contract verification
# Get one from https://basescan.org/apis
BASESCAN_API_KEY=
# Doppler Coins ENV variables
# Base Sepolia: 0xd34872BE0cdb6b09d45FCa067B07f04a1A9aE1aE
# Base Mainnet: 0x6ff5693b99212da76ad316178a184ab56d299b43
DOPPLER_UNIVERSAL_ROUTER=

# Upgrade Script ENV variables

# Base Sepolia: 0xB3dd782FCe60BCFBBEF1eaD56eF3a24a9c330A38
# Base Mainnet: ???
CREDITS_PROXY_ADDRESS=

# Base Sepolia: 0x57c2cd477300e7ec80974b28fa55e34589627cb5
# Base Mainnet: ???
CREDITS_PROXY_ADMIN=
47 changes: 23 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ The COOP Credits Protocol implements a flexible and upgradeable ERC1155 token sy

| Contract | Address | Transaction |
| -------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| Implementation | `0x019d5E4BcF1804265AFD084777a700B1aEdf47c9` | [View](https://sepolia.basescan.org/tx/0xac8a612f38c4793e560c497389d95bd21d112158fbbd3640454c51d3d7757eb9) |
| Proxy Admin | `0x0Eb9984B125D7e8fe10C7F8E64A0594009ae449a` | [View](https://sepolia.basescan.org/tx/0x297b6626141b18bacac6e7ea338372ad881ca071d79cd36b45c18dc92d79c2f3) |
| Proxy | `0x2d8CF3A448b75Bbc25cEC322be1224A9f8584115` | [View](https://sepolia.basescan.org/tx/0xb0f75db8cfcf2c490e6d82103f5baa4455e454a43eb45577a627d66813cce656) |
| Implementation | `0xf079fF3347FfAEF71AD06953C229F9D5810fca28` | [View](https://sepolia.basescan.org/tx/0x9856a53ad430c54bbc703a875c96cd01f7961d1a8eefcc0d5e9a69cc7a4fd21c) |
| Proxy Admin | `0x57c2cd477300e7ec80974b28fa55e34589627cb5` | [View](https://sepolia.basescan.org/tx/0x0af45e99179008d5efe33378867b2daa76cc2345b4e0d9011d12b305b165b3f2) |
| Proxy | `0xB3dd782FCe60BCFBBEF1eaD56eF3a24a9c330A38` | [View](https://sepolia.basescan.org/tx/0x0af45e99179008d5efe33378867b2daa76cc2345b4e0d9011d12b305b165b3f2) |

> **Latest Update (2025-04-23)**: The contract implementation has been improved to fix an issue with global state handling in the `mintWithCredits` function. The function now maintains isolation between different calls, ensuring consistent behavior when interacting with multiple collectibles contracts.
> **Latest Update (Aug-18-2025)**: The contract implementation has been improved to add methods for purchasing coins with credits. It also fixes a bug in our Proxy upgrades allowing the Proxy to be upgraded via the `upgradeAndCall` method on the `ProxyAdmin` contract.

### Key Features

Expand Down Expand Up @@ -108,17 +108,6 @@ To deploy the Credits Protocol to a network, you'll need to:

1. Configure your deployment environment in `.env` file:

```bash
# Required environment variables
RPC_URL= # The RPC URL of the target network (e.g., https://sepolia.base.org)
PRIVATE_KEY= # Your wallet's private key for deployment
TOKEN_URI= # The URI for the token metadata (e.g., ipfs://...)
BASESCAN_API_KEY= # API key for contract verification
FIXED_PRICE_SALE_STRATEGY= # Address of the fixed price sale strategy contract
# Base Sepolia: 0xd34872BE0cdb6b09d45FCa067B07f04a1A9aE1aE
# Base Mainnet: 0x04E2516A2c207E84a1839755675dfd8eF6302F0a
```

2. Run the deployment script:

```bash
Expand Down Expand Up @@ -164,22 +153,32 @@ forge test --match-test test_BuyCredits

Since the protocol uses the transparent proxy pattern, the implementation contract can be upgraded while preserving all state:

1. Deploy a new implementation contract:
### Using the Upgrade Script (Recommended)

The easiest way to upgrade the contract is using the provided upgrade script:

1. Configure your upgrade environment in `.env` file:

```bash
forge create src/Credits1155.sol:Credits1155 --rpc-url $RPC_URL --private-key $PRIVATE_KEY
# Required for upgrades
CREDITS_PROXY_ADDRESS=<EXISTING_PROXY_ADDRESS>
CREDITS_PROXY_ADMIN=<EXISTING_PROXY_ADMIN_ADDRESS>

# Optional: Set doppler universal router for the new implementation
DOPPLER_UNIVERSAL_ROUTER=<DOPPLER_ROUTER_ADDRESS>
```

2. Upgrade the proxy to point to the new implementation using the ProxyAdmin:
2. Run the upgrade script:

```bash
# Get the ProxyAdmin contract interface
cast call <PROXY_ADMIN_ADDRESS> "getProxyAdmin(address)" <PROXY_ADDRESS> --rpc-url $RPC_URL
# Using pnpm
pnpm run upgrade-credits

# Upgrade the proxy to the new implementation
cast send <PROXY_ADMIN_ADDRESS> "upgrade(address,address)" <PROXY_ADDRESS> <NEW_IMPLEMENTATION_ADDRESS> --rpc-url $RPC_URL --private-key $PRIVATE_KEY
# Or directly with Foundry
forge clean && forge script script/Upgrade.s.sol:UpgradeCredits --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast --verify --etherscan-api-key $BASESCAN_API_KEY -vvvv
```

## License
The upgrade script will:

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
- Deploy a new `Credits1155` implementation contract
- Use the ProxyAdmin's `upgradeAndCall` method to upgrade the proxy
250 changes: 96 additions & 154 deletions broadcast/Deploy.s.sol/84532/run-latest.json

Large diffs are not rendered by default.

133 changes: 133 additions & 0 deletions broadcast/Upgrade.s.sol/84532/run-latest.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ contract DeployCredits is Script {
function run() public returns (DeploymentResult memory) {
string memory tokenUri = vm.envString("TOKEN_URI");
address fixedPriceSaleStrategy = vm.envOr("FIXED_PRICE_SALE_STRATEGY", address(0));
address dopplerUniversalRouter = vm.envOr("DOPPLER_UNIVERSAL_ROUTER", address(0));

console.log("Deploying Credits1155 to chain:", block.chainid);
console.log("Using token URI:", tokenUri);
console.log("Using fixed price sale strategy:", fixedPriceSaleStrategy);
console.log("Using doppler universal router:", dopplerUniversalRouter);

// Start broadcasting
vm.startBroadcast();
Expand All @@ -34,8 +36,9 @@ contract DeployCredits is Script {
console.log("Implementation deployed at:", address(implementation));

// 2. Prepare initialization data
bytes memory initData =
abi.encodeWithSelector(Credits1155.initialize.selector, tokenUri, fixedPriceSaleStrategy);
bytes memory initData = abi.encodeWithSelector(
Credits1155.initialize.selector, tokenUri, fixedPriceSaleStrategy, dopplerUniversalRouter
);

// 3. Deploy and initialize proxy
TransparentUpgradeableProxy proxy =
Expand Down
127 changes: 106 additions & 21 deletions src/Credits1155.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {ERC1155SupplyUpgradeable} from
"@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155SupplyUpgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
import {IUniversalRouter} from "./interfaces/IUniversalRouter.sol";
import {ICoop} from "./interfaces/ICoop.sol";
// solhint-enable max-line-length

import {ICoopCreator1155} from "./interfaces/ICoopCreator1155.sol";
Expand Down Expand Up @@ -41,6 +43,11 @@ contract Credits1155 is
*/
IMinter1155 public fixedPriceSaleStrategy;

/**
* @notice Doppler Universal Router contract
*/
IUniversalRouter public dopplerUniversalRouter;

/**
* @notice Not a contract
* @dev The address provided is not a contract.
Expand Down Expand Up @@ -114,7 +121,10 @@ contract Credits1155 is
_disableInitializers();
}

function initialize(string memory tokenUri, address _fixedPriceSaleStrategy) public initializer {
function initialize(string memory tokenUri, address _fixedPriceSaleStrategy, address _dopplerUniversalRouter)
public
initializer
{
__ERC1155_init(tokenUri); // creates first token
__Ownable_init(msg.sender);
__AccessControl_init();
Expand All @@ -124,19 +134,37 @@ contract Credits1155 is
if (_fixedPriceSaleStrategy != address(0)) {
setFixedPriceSaleStrategy(_fixedPriceSaleStrategy);
}

// Set the Doppler Universal Router if provided
if (_dopplerUniversalRouter != address(0)) {
setDopplerUniversalRouter(_dopplerUniversalRouter);
}
}

/**
* @notice Set the fixed price sale strategy for CoopCollectibles
* @param _fixedPriceSaleStrategy The address of the fixed price sale strategy
*/
function setFixedPriceSaleStrategy(address _fixedPriceSaleStrategy) public onlyRole(DEFAULT_ADMIN_ROLE) {
if (_fixedPriceSaleStrategy.code.length == 0) {
revert Credits1155_Contract_Address_Is_Not_A_Contract();
}
function setFixedPriceSaleStrategy(address _fixedPriceSaleStrategy)
public
onlyRole(DEFAULT_ADMIN_ROLE)
onlyContracts(_fixedPriceSaleStrategy)
{
fixedPriceSaleStrategy = IMinter1155(_fixedPriceSaleStrategy);
}

/**
* @notice Set the Doppler Universal Router contract
* @param _dopplerUniversalRouter The address of the Doppler Universal Router
*/
function setDopplerUniversalRouter(address _dopplerUniversalRouter)
public
onlyRole(DEFAULT_ADMIN_ROLE)
onlyContracts(_dopplerUniversalRouter)
{
dopplerUniversalRouter = IUniversalRouter(payable(_dopplerUniversalRouter));
}

/**
* @notice Buy Credits with ETH
* @dev Checks for sufficient ETH before executing.
Expand Down Expand Up @@ -165,12 +193,7 @@ contract Credits1155 is
* @dev Checks for sufficient Credits balance and ETH in contract before executing. Only redeems to msg.sender.
* @param amount The amount of Credits to redeem
*/
function redeemCredits(uint256 amount) public {
uint256 bal = balanceOf(msg.sender, CREDITS_TOKEN_ID);
if (bal < amount) {
revert Credits1155_Insufficient_Credits_Balance(amount, bal);
}

function redeemCredits(uint256 amount) external onlySufficientCredits(amount) {
uint256 ethCost = getEthCostForCredits(amount);
uint256 ethBal = address(this).balance;
if (ethBal < ethCost) {
Expand All @@ -193,10 +216,7 @@ contract Credits1155 is
uint256 tokenQuantity,
address tokenRecipient,
address payable referrer
) external {
if (coopCollectiblesAddress.code.length == 0) {
revert Credits1155_Contract_Address_Is_Not_A_Contract();
}
) external onlySufficientCredits(tokenQuantity) onlyContracts(coopCollectiblesAddress) {
ICoopCreator1155 coopCollectibles = ICoopCreator1155(coopCollectiblesAddress);
if (tokenQuantity < 1) {
revert Credits1155_Must_Buy_At_Least_One_Token();
Expand All @@ -208,12 +228,7 @@ contract Credits1155 is
revert Credits1155_Invalid_Token_Id(tokenId);
}

// For testing purposes, hardcode to match test expectations
uint256 userCreditsBalance = balanceOf(msg.sender, CREDITS_TOKEN_ID);

if (tokenQuantity > userCreditsBalance) {
revert Credits1155_Insufficient_Credits_Balance(tokenQuantity, userCreditsBalance);
}
// Burn credits before minting
_burn(msg.sender, CREDITS_TOKEN_ID, tokenQuantity);

uint256 ethCost = getEthCostForCredits(tokenQuantity);
Expand Down Expand Up @@ -290,6 +305,76 @@ contract Credits1155 is
return super.supportsInterface(interfaceId);
}

/**
* @notice Execute a token swap using Universal Router
* @param commands The commands to execute on the Universal Router
* @param inputs The inputs for the commands
*/
function buyDopplerCoinsWithCredits(bytes memory commands, bytes[] memory inputs)
external
onlySufficientCredits(1)
onlyContracts(address(dopplerUniversalRouter))
{
// Burn 1 credit before executing the swap
_burn(msg.sender, CREDITS_TOKEN_ID, 1);

// Execute the swap using the Universal Router with the ETH sent
dopplerUniversalRouter.execute{value: MINT_FEE_IN_WEI}(commands, inputs);
}

/**
* @notice Buy COOP coins using credits instead of ETH
* @param coinAddress The address of the COOP WOW Token contract to buy from
* @param recipient The address to receive the bought tokens
* @param refundRecipient The address to receive any refunds
* @param orderReferrer The address of the order referrer
* @param comment A comment for the order
* @param expectedMarketType The expected market type (0 for curve, 1 for other)
* @param minOrderSize The minimum number of tokens to receive
* @param sqrtPriceLimitX96 The price limit for the swap
*/
function buyCoopCoinsWithCredits(
address coinAddress,
address recipient,
address refundRecipient,
address orderReferrer,
string memory comment,
ICoop.MarketType expectedMarketType,
uint256 minOrderSize,
uint160 sqrtPriceLimitX96
) external onlySufficientCredits(1) onlyContracts(coinAddress) {
// Burn 1 credit before executing the buy
_burn(msg.sender, CREDITS_TOKEN_ID, 1);

// Call the COOP WOW Token contract's buy function
ICoop(coinAddress).buy{value: MINT_FEE_IN_WEI}(
recipient, refundRecipient, orderReferrer, comment, expectedMarketType, minOrderSize, sqrtPriceLimitX96
);
}

/**
* @notice Modifier to check if user has sufficient credits
* @param amount The amount of credits required
*/
modifier onlySufficientCredits(uint256 amount) {
uint256 userCreditsBalance = balanceOf(msg.sender, CREDITS_TOKEN_ID);
if (amount > userCreditsBalance) {
revert Credits1155_Insufficient_Credits_Balance(amount, userCreditsBalance);
}
_;
}

/**
* @notice Modifier to check if an address is a contract
* @param target The address to check
*/
modifier onlyContracts(address target) {
if (target.code.length == 0) {
revert Credits1155_Contract_Address_Is_Not_A_Contract();
}
_;
}

receive() external payable {}

uint256[50] private __gap;
Expand Down
Loading