Skip to content

Commit

Permalink
Merge pull request #39 from sunrise-stake/update-readme
Browse files Browse the repository at this point in the history
Update README.md
  • Loading branch information
dankelleher authored Sep 4, 2024
2 parents 47a1a06 + af8cee6 commit 988b521
Show file tree
Hide file tree
Showing 18 changed files with 217 additions and 72 deletions.
93 changes: 66 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,82 @@ and uses them to buy and retire carbon credits via Toucan.

The resultant Retirement Certificate is then bridged back to Solana.

## Instructions
## Process Description

Note: This is a work in progress, and these steps will change as the project evolves.
1. Depositing into a token authority account on Solana
A token authority account is an account on Solana where users can deposit funds into that which go through the offset bridge to retire carbon credits. The token authority account is a PDA owned by the swap-bridge program and specified through a state account on Solana. Within the state account, a holding contract on polygon, to which the fund from solana will be bridged to, is specified. Each holding contract is generated for a specific NCT carbon project and a specific Solana wallet to which the retirement certificate will be stored at the end of the process.

1. Initialize
Hence each token authority account corresponds to a specific carbon project and a specific Solana wallet. Therefore, each user should create their own token authority account if they would like the retirement certificate to be sent back to their own wallet.

```shell
yarn initialize
```
2. Swapping into wrapped USDC (USDCpo)
The fund in the token authority account will then be swapped into USDCpo on a DEX (Jupiter is used currently). This is done such that the funds can be sent to Polygon to buy and retire NCT carbon credits, which are on the Polygon chain.

Sets up a new State on solana and derives a Token Authority PDA.
This should be done only once.
3. Bridging into a holding contract on Polygon
The USDCpo now in the token authority account will be sent to Polygon via the Wormhole bridge in this step. The bridged fund will land as USDC on the holding contract that is associated with the token authority account. Shall it happen that the user wish to bridge the funds existing in the token authority account to a different account on Polygon, it is possible with the signature of the update authority.

2. Wrap SOL
4. Retiring carbon credits
In this step the USDC now in the holding contract will be used to buy NCT carbon credits through a DEX (Sushi Swap is used in this step currently). The NCT carbon credits will then be sent to a retirement contract where TCO2 will be retired and a retirement certificate will be minted at the same time. The beneficiary of the retirement certificate is

```shell
yarn wrap-sol
```
5. Bridging back to Solana
The retirement certificate will be bridged back to Solana to the wallet specified in the state account that is associated with the token authority account in the first step.

Transfers SOL to the Solana "Token Authority" PDA

NOTE: this step will later be moved into the smart contract, so you can simply call the smart contract with SOL.
NOTE 2: When paying with an SPL Token like USDC, this step can be skipped
## UI
A user interface is available for going through the above process easily, the UI consists on the following pages:

3. Swap SOL for USDCpo
1. Step 1 -- Select Account
In this page the user can connect with their own Solana and Polygon wallets. The Solana wallet should contain either the SOL or USDC which are going to be used to buy the NCT carbon tokens. The Polygon wallet should contain enough Matic for paying transaction fees needed in the later steps.

```shell
yarn swap
```
A default carbon project is displayed on screen but users can also pick their own carbon project to retire credits for by clicking the button `Change`. As of now, you can choose from multiple Toucan projects/carbon offsets. In the future, more projects from other third parties will be accessible via Sunrise Stake’s offset bridge.

4. Bridge USDCpo to Polygon and receive USDC in Holding Contract
![step1b.png](docs/step1b.png)
![step1c.png](docs/step1c.png)

```shell
yarn bridge
```
As an example, let us pick [VCS875 2015](https://registry.verra.org/app/projectDetail/VCS/875), a REDD+ (“_Reducing Emissions from Deforestation and forest Degradation_”) project which is also offered by Regen Network. Once you have selected a project, you can see how many credits are still available to be acquired and retired.

5. Use the funds to retire carbon tokens on Toucan
```shell
yarn retire
```
![step1d.png](docs/step1d.png)

**As of now, the UI does not prevent you from committing more funds to a project, than it has credits left for you to retire and it is not possible to bridge left over funds back, so please double-check.** In future updates, the UI will prevent you from committing more funds than you can actually spend on any carbon project.

After the wallets are connected and the carbon project specified. User can click `Setup Solana side` to set up the token authority account (which is associated with a holding contract owned by the connected Polygon wallet).

2. Step 2 -- Deposit
In this page the user can choose to deposit either USDC or SOL into the token authority account. If you would like to specify the amount of metric tons of carbon you want to retire rather than the amount of SOL or USDC you would like to be spent, you can use the toggle and switch from “SOL” to “carbon”.

![step2a.png](docs/step2a.png)

After specifying the amount of SOL/USDC to deposit, users can click `Deposit and Swap` to perform both the action of depositing the SOL/USDC from the Solana wallet to the token authority account and swapping the deposited amount into wrapped USDC (USDCpo) on the DEX (Jupiter). The amount of tCO2 that could be retired with the deposited amount will be displayed in bracket.

Effectively, when you send funds into the state contract on Solana, you are committing them to solely be spent in the holding contract on Polygon. This holding contract specifies the respective carbon project you choose to retire carbon offsets against. Once you have committed funds to the state account on Solana, it can only be spent by the holding account on Polygon for the carbon project you have chosen.


3. Step 3 -- Bridge
The total amount existing in the token authority account will be displayed on this page. The user can check if it's the expected amount before clicking `Bridge` to send the USDCpo from the token authority account to the bridge (Wormhole).

![step3a.png](docs/step3a.png)

After the amount has been sent sucessfully to the bridge, the `Redeem` button will turn green and the user can then click on it to redeem the sent USDCpo onto the holding contract on the Polygon chain to finish the bridging action.

![step3b.png](docs/step3b.png)

4. Step 4 -- Retire
Now that the USDCs are in the holding contract, it is ready to be used to retire carbon credit. In this page users would just need to click `Retire` and all the USDCs in the holding contract will be used to retire carbon credits from the selected carbon project in Step 1. An NFT will be minted at the same time that represent the retirement certificate.

![step4a.png](docs/step4a.png)

5. Step 5 -- Redeem Retirement Certificate
The retirement certificate can be bridged back to the user's Solana wallet. Similar to Step 3, the user would have to first click `Bridge` to send the NFT to the bridge and then once the NFT is sent to the bridge, the user can click the `Redeem` button to redeem it onto their Solana wallet.

![step5a.png](docs/step5a.png)

This might take a few minutes or hours, as Wormhole slows down bridging assets from Polygon for security reasons. After that, you can redeem the certificate on Solana by pressing the respective button and confirming the transactions in your Solana wallet and go to the last step by clicking “next”.

![step5b.png](docs/step5b.png)

6. Step 6 -- Retirement Certificate
On this page all the exisiting Toucan retirement certificates in the user's Solana wallet will be displayed, a new retirement certificate should be shown after going through the above steps. To see the metadata of the retirement certificate, you can click on the hyperlink in the “View” page.

![step6a.png](docs/step6a.png)
![step6b.png](docs/step6b.png)

Congratulations! You have successfully offset some carbon and bridged the retirement proof onto Solana.
Binary file added docs/step1b.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/step1c.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/step1d.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/step2a.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/step3a.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/step3b.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/step4a.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/step5a.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/step5b.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/step6a.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/step6b.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 26 additions & 21 deletions evm-contract/src/CarbonOffsetSettler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import "lib/openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol";
import "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC721/IERC721Upgradeable.sol";
import "forge-std/console.sol";

/**
* @notice This contract is to be used to purchase and offset carbon credits
*/
contract CarbonOffsetSettler is OwnableUpgradeable, IERC721Receiver {
address public constant NCT = 0xD838290e877E0188a4A44700463419ED96c16107;
address public constant CERT = 0x5e377f16E4ec6001652befD737341a28889Af002;
Expand All @@ -19,18 +22,15 @@ contract CarbonOffsetSettler is OwnableUpgradeable, IERC721Receiver {

uint public tokenId;

// function initialize(
// address newholdingContract,
// address sunriseAdmin
// ) external {
// transferOwnership(sunriseAdmin);
// holdingContract = newholdingContract;
// }

// function setHoldingContract(address newHoldingContract) external onlyOwner {
// holdingContract = newHoldingContract;
// }

/**
* Retire carbon credits from specified project for a specified amount of USDC
* @param _tco2 Address of the project for which TCO2 will be retired
* @param _amountUSDC Amount of USDC to be used to buy carbon credits
* @param _entity Name of the entity who is performing the retirement
* @param _beneficiary Address of beneficiary, to whom the retirement will be accredited
* @param _beneficiaryName Name of beneficiary
* @param _msg Any message to be attached to the retirement certificate
*/
function retire(
address _tco2,
uint256 _amountUSDC,
Expand Down Expand Up @@ -72,7 +72,7 @@ contract CarbonOffsetSettler is OwnableUpgradeable, IERC721Receiver {
);
}

/*
/**
* @notice Called when new Toucan retirement certificate NFTs are minted.
*/
function onERC721Received(
Expand All @@ -85,9 +85,10 @@ contract CarbonOffsetSettler is OwnableUpgradeable, IERC721Receiver {
return IERC721Receiver.onERC721Received.selector;
}

/*
* Given a specific TCO2 and amount, return the amount of xUSDC we would
* need to burn after fees.
/**
* Given a specific TCO2 and amount, return the amount of xUSDC we would need to burn after fees.
* @param _tco2 Address of the project for which TCO2 will be retired
* @param _amountToOffset Amount of TCO2 to offset
*/
function getUSDCNeeded(
address _tco2,
Expand All @@ -111,9 +112,10 @@ contract CarbonOffsetSettler is OwnableUpgradeable, IERC721Receiver {
return expectedAmountsIn[0];
}

/*
* Given a specific TCO2 and amount, return the amount of xUSDC we would
* need to burn after fees.
/**
* Given a specific TCO2 and amount, return the amount of NCT we would need to burn after fees.
* @param _tco2 Address of the project for which TCO2 will be retired
* @param _amountToOffset Amount of TCO2 to offset
*/
function getExpectedNCT(
address _tco2,
Expand All @@ -135,8 +137,10 @@ contract CarbonOffsetSettler is OwnableUpgradeable, IERC721Receiver {
return expectedAmountsIn[0];
}

/*
* Only support USDC <-> NCT routes
/**
* Find path for swapping; only support USDC <-> NCT routes
* @param _from Address of token to be swapped from
* @param _to Address of token to be swapped to
*/
function generatePath(
address _from,
Expand All @@ -150,6 +154,7 @@ contract CarbonOffsetSettler is OwnableUpgradeable, IERC721Receiver {

/**
* Swap USDC on contract into NCT.
* @param _amountUSDC Amount of USDC to swap for NCT tokans
*/
function swap(uint256 _amountUSDC) public returns (uint256) {
IUniswapV2Router02 routerSushi = IUniswapV2Router02(SUSHI_ROUTER);
Expand Down
39 changes: 26 additions & 13 deletions evm-contract/src/HoldingContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC721/IERC721Upg
import "lib/openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol";
import "lib/openzeppelin-contracts/contracts/utils/Counters.sol";

/**
* A holding contract that holds the bridged funds from a Solana account, use it to buy and retire TCO2,
* and send the retirement certificate back to the Solana address
*/
contract HoldingContract is OwnableUpgradeable, IERC721Receiver {
// Logic contract address
// address public logicContract;
Expand All @@ -37,7 +41,13 @@ contract HoldingContract is OwnableUpgradeable, IERC721Receiver {
// USDC token contract
address public constant USDC = 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174;

// Event emitted when funds are used to offset emissions
/**
* Event emitted when funds are used to offset emissions
* @param tco2 Address of the project for which TCO2 will be retired
* @param beneficiary Address of beneficiary, to whom the retirement will be accredited
* @param beneficiaryName Name of beneficiary
* @param amount Amount of TCO2 offset
*/
event Offset(
address indexed tco2,
address indexed beneficiary,
Expand All @@ -49,8 +59,15 @@ contract HoldingContract is OwnableUpgradeable, IERC721Receiver {
error InsufficientFunds();
error InvalidRetireContract();

/*
/**
* To initialize a holding contract with a new set of parameters
* @notice Initialize the proxy contract
* @param newTco2 Address of the project for which TCO2 will be retired
* @param newBeneficiary Address of beneficiary, to whom the retirement will be accredited
* @param newBeneficiaryName Name of beneficiary
* @param newSolanaAccountAddress Address of wallet in Solana to which the retirement certificate will be sent to
* @param newRetireContract Address of the deployed contract of CarbonOffsetSettler to be used to retire TCO2
* @param owner Address of owner of the holding contract
*/
function initialize(
address newTco2,
Expand All @@ -70,22 +87,18 @@ contract HoldingContract is OwnableUpgradeable, IERC721Receiver {
transferOwnership(owner);
}

/*
/**
* @notice Set the address of the CarbonOffsetSettler contract
* @param newRetireContract address of the CarbonOffsetSettler contract
* @param newRetireContract Address of the CarbonOffsetSettler contract
*/
function setRetireContract(address newRetireContract) external onlyOwner {
if (newRetireContract == address(0)) revert InvalidRetireContract();
retireContract = newRetireContract;
}

// function setFactoryContract(address newFactoryContract) external onlyOwner {
// factoryContract = newFactoryContract;
// }

// ------------------ Setting retirement details (admin-only, ensure valid values) ------------------ //

/*
/**
* @notice Set the details for the beneficiary of the carbon offset
* @param newBeneficiary address of the beneficiary (in case you're retiring for someone else)
* @param newBeneficiaryName name of the beneficiary
Expand All @@ -98,7 +111,7 @@ contract HoldingContract is OwnableUpgradeable, IERC721Receiver {
beneficiaryName = newBeneficiaryName;
}

/*
/**
* @notice Set the address of the TCO2 token contract
* @param newTco2 address of the TCO2 token contract
*/
Expand All @@ -118,7 +131,7 @@ contract HoldingContract is OwnableUpgradeable, IERC721Receiver {
}

// --------------------------------- Permissionless retiring --------------------------------------- //
/*
/**
* @notice Offset carbon emissions by sending USDC to the CarbonOffsetSettler contract and using the funds to retire carbon tokens.
* @param entity name of the entity that does the retirement (Sunrise Stake)
* @param message retirement message
Expand Down Expand Up @@ -158,7 +171,7 @@ contract HoldingContract is OwnableUpgradeable, IERC721Receiver {
_bridgeNFT(tokenId);
}

/*
/**
* @dev Necessary to receive ERC721 transfers
*/
function onERC721Received(
Expand All @@ -170,7 +183,7 @@ contract HoldingContract is OwnableUpgradeable, IERC721Receiver {
return IERC721Receiver.onERC721Received.selector;
}

/*
/**
* @notice Set the Solana Account that receives the retirement certificate
* @dev This is read out to calculate the associated token account for the Wormhole message
* @param newSolanaAccountAddress address of the Solana Account
Expand Down
21 changes: 18 additions & 3 deletions evm-contract/src/HoldingContractFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import "src/HoldingContract.sol";
import "forge-std/console.sol";
import "lib/openzeppelin-contracts/contracts/access/Ownable.sol";

/**
* @notice Factory to generate holding contracts
*/
contract HoldingContractFactory is Ownable {
address public implementationAddress;
address[] public proxies;
Expand All @@ -16,12 +19,24 @@ contract HoldingContractFactory is Ownable {
implementationAddress = newImplementationAddress;
}

/**
* @notice Set the implementation address for a holding contract
* @param newImplementationAddress Address of the implementation of a holding contract
*/
function setImplementationAddress(
address newImplementationAddress
) external onlyOwner {
implementationAddress = newImplementationAddress;
}

/**
* Create a new holding contract
* @param salt Salt for determining the address of new holding contract
* @param newTco2 Address of the project for which TCO2 will be retired
* @param newBeneficiaryName Name of beneficiary
* @param newSolanaAccountAddress Address of wallet in Solana to which the retirement certificate will be sent to
* @param retireContract Address of the deployed contract of CarbonOffsetSettler to be used to retire TCO2
*/
function createContract(
bytes32 salt,
address newTco2,
Expand All @@ -47,9 +62,9 @@ contract HoldingContractFactory is Ownable {
}

/**
* @dev Returns the address of the implementation given a particular salt
* @return The address of the implementation.
*/
* @dev Returns the address of the implementation given a particular salt
* @return The address of the implementation.
*/
function getContractAddress(
bytes32 salt,
address deployer
Expand Down
Loading

0 comments on commit 988b521

Please sign in to comment.