Skip to content

Commit 0e28664

Browse files
committed
Add fuzzing tests and sync with tokenizer foundry config
1 parent 3fb879a commit 0e28664

File tree

5 files changed

+731
-3
lines changed

5 files changed

+731
-3
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ cache
1010
artifacts
1111

1212
# Foundry files
13+
cache_forge
1314
out
1415

1516
*.log

foundry.toml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
[profile.default]
2-
src = "contracts"
3-
out = "out"
4-
libs = ["node_modules"]
2+
src = 'contracts'
3+
out = 'out'
4+
libs = ['node_modules', 'lib']
5+
test = 'test'
6+
cache_path = 'cache_forge'
57
optimizer = true
68
optimizer_runs = 200
9+
show_progress = true
710

811
[fuzz]
912
runs = 1024
1013
max_test_rejects = 524288
14+
fail_on_revert = true
1115

1216
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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

Comments
 (0)