|
| 1 | +// SPDX-FileCopyrightText: 2024 Toucan Labs |
| 2 | +// |
| 3 | +// SPDX-License-Identifier: LicenseRef-Proprietary |
| 4 | +pragma solidity ^0.8.13; |
| 5 | + |
| 6 | +import '@openzeppelin/contracts-upgradeable/proxy/ClonesUpgradeable.sol'; |
| 7 | +import '@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol'; |
| 8 | +import '@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol'; |
| 9 | +import '@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol'; |
| 10 | +import 'forge-std/Test.sol'; |
| 11 | + |
| 12 | +import '../../../contracts/PuroToucanCarbonOffsets.sol'; |
| 13 | +import '../../../contracts/CarbonOffsetBatches.sol'; |
| 14 | +import '../../../contracts/PuroToucanCarbonOffsetsFactory.sol'; |
| 15 | +import '../../../contracts/ToucanContractRegistry.sol'; |
| 16 | +import '../../../contracts/CarbonProjectVintages.sol'; |
| 17 | +import '../../../contracts/CarbonProjects.sol'; |
| 18 | +import '../../../contracts/ToucanCarbonOffsetsEscrow.sol'; |
| 19 | +import '../../../contracts/retirements/RetirementCertificates.sol'; |
| 20 | +import './ToucanCarbonOffsetsHandler.sol'; |
| 21 | + |
| 22 | +/** |
| 23 | + * This Smart Contract is responsible for: |
| 24 | + * - setting up the part of the protocol relevant for the testing of the given invariant |
| 25 | + * - instantiating a handler to drive the fuzzing |
| 26 | + * - checking the invariant: TCO2 tokens are backed 1:1 by batches. |
| 27 | + * The contract can be extended to verify more variants that would require the same setup. |
| 28 | + */ |
| 29 | +contract Tco2BackedByBatchesInvariant is Test { |
| 30 | + ToucanContractRegistry toucanRegistry; |
| 31 | + CarbonProjectVintages vintages; |
| 32 | + CarbonProjects projects; |
| 33 | + CarbonOffsetBatches batches; |
| 34 | + PuroToucanCarbonOffsetsFactory puroTco2Factory; |
| 35 | + ToucanCarbonOffsetsEscrow toucanCarbonOffsetsEscrow; |
| 36 | + RetirementCertificates retirementCertificates; |
| 37 | + |
| 38 | + ToucanCarbonOffsetsHandler tco2Handler; |
| 39 | + |
| 40 | + function setUp() external { |
| 41 | + tco2Handler = new ToucanCarbonOffsetsHandler(); |
| 42 | + |
| 43 | + _setupToucanRegistry(); |
| 44 | + _setupProjects(); |
| 45 | + _setupVintages(); |
| 46 | + _setupBatches(); |
| 47 | + _setupToucanCarbonOffsetsFactory(); |
| 48 | + _setupToucanCarbonOffsets(); |
| 49 | + _setupToucanCarbonOffsetsEscrow(); |
| 50 | + _setupRetirementCertificates(); |
| 51 | + |
| 52 | + // last configuration steps |
| 53 | + puroTco2Factory.transferOwnership(address(tco2Handler)); |
| 54 | + tco2Handler.configure(projects, vintages, batches, puroTco2Factory, address(this)); |
| 55 | + |
| 56 | + // limit the fuzzer scope |
| 57 | + targetContract(address(tco2Handler)); |
| 58 | + bytes4[] memory selectors = new bytes4[](7); |
| 59 | + selectors[0] = tco2Handler.addTCO2.selector; |
| 60 | + selectors[1] = tco2Handler.tokenize.selector; |
| 61 | + selectors[2] = tco2Handler.requestRetirement.selector; |
| 62 | + selectors[3] = tco2Handler.finalizeRetirement.selector; |
| 63 | + selectors[4] = tco2Handler.requestDetokenization.selector; |
| 64 | + selectors[5] = tco2Handler.finalizeDetokenization.selector; |
| 65 | + selectors[6] = tco2Handler.defractionalize.selector; |
| 66 | + targetSelector(FuzzSelector(address(tco2Handler), selectors)); |
| 67 | + } |
| 68 | + |
| 69 | + function _setupToucanRegistry() internal { |
| 70 | + toucanRegistry = new ToucanContractRegistry(); |
| 71 | + excludeContract(address(toucanRegistry)); |
| 72 | + address[] memory accounts = new address[](2); |
| 73 | + accounts[0] = accounts[1] = address(this); |
| 74 | + bytes32[] memory roles = new bytes32[](2); |
| 75 | + roles[0] = toucanRegistry.PAUSER_ROLE(); |
| 76 | + roles[1] = toucanRegistry.DEFAULT_ADMIN_ROLE(); |
| 77 | + |
| 78 | + ERC1967Proxy proxy = new ERC1967Proxy( |
| 79 | + address(toucanRegistry), |
| 80 | + abi.encodeWithSignature('initialize(address[],bytes32[])', accounts, roles) |
| 81 | + ); |
| 82 | + toucanRegistry = ToucanContractRegistry(address(proxy)); |
| 83 | + } |
| 84 | + |
| 85 | + function _setupProjects() internal { |
| 86 | + projects = new CarbonProjects(); |
| 87 | + excludeContract(address(projects)); |
| 88 | + address[] memory accounts = new address[](3); |
| 89 | + accounts[0] = accounts[1] = address(this); |
| 90 | + accounts[2] = address(tco2Handler); |
| 91 | + bytes32[] memory roles = new bytes32[](3); |
| 92 | + roles[0] = projects.MANAGER_ROLE(); |
| 93 | + roles[1] = projects.DEFAULT_ADMIN_ROLE(); |
| 94 | + roles[2] = projects.MANAGER_ROLE(); |
| 95 | + ERC1967Proxy proxy = new ERC1967Proxy( |
| 96 | + address(projects), |
| 97 | + abi.encodeWithSignature('initialize(address[],bytes32[])', accounts, roles) |
| 98 | + ); |
| 99 | + projects = CarbonProjects(address(proxy)); |
| 100 | + projects.setToucanContractRegistry(address(toucanRegistry)); |
| 101 | + toucanRegistry.setCarbonProjectsAddress(address(projects)); |
| 102 | + } |
| 103 | + |
| 104 | + function _setupVintages() internal { |
| 105 | + vintages = new CarbonProjectVintages(); |
| 106 | + excludeContract(address(vintages)); |
| 107 | + address[] memory accounts = new address[](3); |
| 108 | + accounts[0] = accounts[1] = address(this); |
| 109 | + accounts[2] = address(tco2Handler); |
| 110 | + bytes32[] memory roles = new bytes32[](3); |
| 111 | + roles[0] = vintages.MANAGER_ROLE(); |
| 112 | + roles[1] = vintages.DEFAULT_ADMIN_ROLE(); |
| 113 | + roles[2] = vintages.MANAGER_ROLE(); |
| 114 | + ERC1967Proxy proxy = new ERC1967Proxy( |
| 115 | + address(vintages), |
| 116 | + abi.encodeWithSignature('initialize(address[],bytes32[])', accounts, roles) |
| 117 | + ); |
| 118 | + vintages = CarbonProjectVintages(address(proxy)); |
| 119 | + vintages.setToucanContractRegistry(address(toucanRegistry)); |
| 120 | + toucanRegistry.setCarbonProjectVintagesAddress(address(vintages)); |
| 121 | + } |
| 122 | + |
| 123 | + function _setupBatches() internal { |
| 124 | + CarbonOffsetBatches implBatches = new CarbonOffsetBatches(); |
| 125 | + excludeContract(address(implBatches)); |
| 126 | + |
| 127 | + ERC1967Proxy proxy = new ERC1967Proxy( |
| 128 | + address(implBatches), |
| 129 | + abi.encodeWithSignature('initialize(address)', address(toucanRegistry)) |
| 130 | + ); |
| 131 | + batches = CarbonOffsetBatches(address(proxy)); |
| 132 | + batches.grantRole(batches.VERIFIER_ROLE(), address(this)); |
| 133 | + batches.grantRole(batches.VERIFIER_ROLE(), address(tco2Handler)); |
| 134 | + batches.grantRole(batches.TOKENIZER_ROLE(), address(tco2Handler)); |
| 135 | + batches.setSupportedRegistry('puro', true); |
| 136 | + |
| 137 | + toucanRegistry.setCarbonOffsetBatchesAddress(address(batches)); |
| 138 | + } |
| 139 | + |
| 140 | + function _setupToucanCarbonOffsetsFactory() internal { |
| 141 | + puroTco2Factory = new PuroToucanCarbonOffsetsFactory(); |
| 142 | + address[] memory accounts = new address[](4); |
| 143 | + accounts[0] = address(this); |
| 144 | + accounts[1] = accounts[2] = accounts[3] = address(tco2Handler); |
| 145 | + bytes32[] memory roles = new bytes32[](4); |
| 146 | + roles[0] = puroTco2Factory.DEFAULT_ADMIN_ROLE(); |
| 147 | + roles[1] = puroTco2Factory.DETOKENIZER_ROLE(); |
| 148 | + roles[2] = puroTco2Factory.TOKENIZER_ROLE(); |
| 149 | + roles[3] = (new PuroToucanCarbonOffsets()).RETIREMENT_ROLE(); |
| 150 | + |
| 151 | + ERC1967Proxy proxy = new ERC1967Proxy( |
| 152 | + address(puroTco2Factory), |
| 153 | + abi.encodeWithSelector( |
| 154 | + puroTco2Factory.initialize.selector, |
| 155 | + toucanRegistry, |
| 156 | + accounts, |
| 157 | + roles |
| 158 | + ) |
| 159 | + ); |
| 160 | + puroTco2Factory = PuroToucanCarbonOffsetsFactory(address(proxy)); |
| 161 | + |
| 162 | + toucanRegistry.setToucanCarbonOffsetsFactoryAddress(address(puroTco2Factory)); |
| 163 | + } |
| 164 | + |
| 165 | + function _setupToucanCarbonOffsets() internal { |
| 166 | + PuroToucanCarbonOffsets tco2Beacon = new PuroToucanCarbonOffsets(); |
| 167 | + UpgradeableBeacon beacon = new UpgradeableBeacon(address(tco2Beacon)); |
| 168 | + puroTco2Factory.setBeacon(address(beacon)); |
| 169 | + } |
| 170 | + |
| 171 | + function _setupRetirementCertificates() internal { |
| 172 | + retirementCertificates = new RetirementCertificates(); |
| 173 | + |
| 174 | + ERC1967Proxy proxy = new ERC1967Proxy( |
| 175 | + address(retirementCertificates), |
| 176 | + abi.encodeWithSelector( |
| 177 | + retirementCertificates.initialize.selector, |
| 178 | + toucanRegistry, |
| 179 | + 'test.com' |
| 180 | + ) |
| 181 | + ); |
| 182 | + retirementCertificates = RetirementCertificates(address(proxy)); |
| 183 | + |
| 184 | + toucanRegistry.setRetirementCertificatesAddress(address(retirementCertificates)); |
| 185 | + } |
| 186 | + |
| 187 | + function _setupToucanCarbonOffsetsEscrow() internal { |
| 188 | + toucanCarbonOffsetsEscrow = new ToucanCarbonOffsetsEscrow(); |
| 189 | + address[] memory accounts = new address[](2); |
| 190 | + accounts[0] = accounts[1] = address(this); |
| 191 | + bytes32[] memory roles = new bytes32[](2); |
| 192 | + roles[0] = toucanCarbonOffsetsEscrow.PAUSER_ROLE(); |
| 193 | + roles[1] = toucanCarbonOffsetsEscrow.DEFAULT_ADMIN_ROLE(); |
| 194 | + |
| 195 | + ERC1967Proxy proxy = new ERC1967Proxy( |
| 196 | + address(toucanCarbonOffsetsEscrow), |
| 197 | + abi.encodeWithSelector( |
| 198 | + toucanCarbonOffsetsEscrow.initialize.selector, |
| 199 | + toucanRegistry, |
| 200 | + accounts, |
| 201 | + roles |
| 202 | + ) |
| 203 | + ); |
| 204 | + toucanCarbonOffsetsEscrow = ToucanCarbonOffsetsEscrow(address(proxy)); |
| 205 | + |
| 206 | + toucanRegistry.setToucanCarbonOffsetsEscrowAddress(address(toucanCarbonOffsetsEscrow)); |
| 207 | + } |
| 208 | + |
| 209 | + function onERC721Received( |
| 210 | + address, /* operator */ |
| 211 | + address, /* from */ |
| 212 | + uint256, /* tokenId */ |
| 213 | + bytes calldata /* data */ |
| 214 | + ) external pure returns (bytes4) { |
| 215 | + return this.onERC721Received.selector; |
| 216 | + } |
| 217 | + |
| 218 | + function invariant_tco2BackedByBatches1to1() external payable { |
| 219 | + address[] memory tco2s = puroTco2Factory.getContracts(); |
| 220 | + |
| 221 | + for (uint256 tco2Index = 0; tco2Index < tco2s.length; tco2Index++) { |
| 222 | + PuroToucanCarbonOffsets tco2 = PuroToucanCarbonOffsets(tco2s[tco2Index]); |
| 223 | + uint256 tco2Supply = tco2.totalSupply(); |
| 224 | + uint256 totalBatches = 0; |
| 225 | + for (uint256 i = 0; i < batches.balanceOf(address(tco2)); i++) { |
| 226 | + uint256 tokenId = batches.tokenOfOwnerByIndex(address(tco2), i); |
| 227 | + (, uint256 tokenQuantity, BatchStatus status) = batches.getBatchNFTData(tokenId); |
| 228 | + if ( |
| 229 | + status == BatchStatus.Confirmed || |
| 230 | + status == BatchStatus.DetokenizationRequested || |
| 231 | + status == BatchStatus.RetirementRequested |
| 232 | + ) totalBatches += tokenQuantity * 1e18; |
| 233 | + } |
| 234 | + |
| 235 | + assertEq(tco2Supply, totalBatches); |
| 236 | + } |
| 237 | + } |
| 238 | + |
| 239 | + receive() external payable {} |
| 240 | +} |
0 commit comments