-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
532858f
commit ea89c31
Showing
8 changed files
with
301 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
# Yield Controller | ||
|
||
The Sunrise Stake Yield Controller is a suite of Solana programs that control | ||
the distribution of staking yield from the Sunrise Stake program to climate projects. | ||
|
||
The current yield distribution mechanism of Sunrise buys and retires | ||
[Toucan NCT carbon tokens](https://blog.toucan.earth/announcing-nct-nature-carbon-tonne/), | ||
however the target model is to diversify the yield distribution to a range of climate | ||
projects, controlled by a DAO. | ||
|
||
## History | ||
|
||
### 1. Initial release - Toucan NCT Buy-Burn-Fixxed | ||
|
||
The initial version of the Sunrise Yield Controller used the Buy-Burn-Fixed strategy to | ||
distribute yield. This strategy was implemented in the `buy-burn-fixed` package. | ||
|
||
The `buy_burn_fixed` strategy bought Toucan NCT tokens at a fixed price and burned them. | ||
The Toucan NCT tokens were bridged from Polygon manually via [Wormhole](https://wormhole.com/) and | ||
stored in a PDA owned by the program. | ||
|
||
The price of NCT was manually updated periodically by a Sunrise administrator in order to ensure | ||
a fair price was paid by Sunrise for the Toucan NCT tokens. | ||
|
||
This had a number of downsides: | ||
- The price of NCT was manually updated, which was a centralised process. | ||
- NCT had to be manually bridged from Polygon to Solana, also a centralised process. | ||
- Bridged NCT was burned, rather than retired via Toucan, which reduced the transparency of the process, and | ||
meant that the off-chain registries could not be updated. | ||
- Yield distribution was limited to Toucan NCT tokens. | ||
|
||
### 2. Toucan NCT Buy-Burn-Switchboard | ||
|
||
The second version used a price oracle set up on [Switchboard](https://switchboard.xyz/) to | ||
determine the correct NCT price. This reduced the ability of administrators to control the price, but | ||
did not affect the other downsides of release 1. | ||
|
||
### 3. Offset Bridge | ||
|
||
The third version of the yield controller replaced the buy-burn mechanism for a bridge-buy-retire mechanism | ||
called the Offset Bridge. | ||
|
||
Rather than manually bridging NCT to Solana, and having the yield controller buy and burn it, instead | ||
a new system - the [offset bridge](https://github.com/sunrise-stake/offset-bridge) - was set up to | ||
bridge funds to Polygon, store them in a smart contract wallet, and automatically retire them via Toucan. | ||
|
||
This ensured that NCT was correctly converted to tCO2 tokens and retired against the Toucan registry, | ||
while ensuring the funds remained in control of the protocol (not touching EOA wallets), but nonetheless | ||
had a number of "crank" steps that, while permissionless, still needed manual involvement to perform. | ||
|
||
Details of the Offset Bridge can be found in the [Offset Bridge README](https://github.com/sunrise-stake/offset-bridge/blob/main/README.md). | ||
|
||
At present, this is the current mechanism for yield distribution in Sunrise. | ||
It uses the "Yield Router" program to route yield from the Sunrise Stake program to the Offset Bridge. | ||
|
||
### 4. (Planned) Diversified Yield Router | ||
|
||
The current implementation of the yield controller sends all funds from the Sunrise program directly to | ||
a PDA owned by the Offset Bridge using the Yield Router, however the Yield Router only has one destination, | ||
the offset bridge. | ||
|
||
The next step will be to adapt the router to support additional distribution of yield to a range of | ||
climate projects, rather than just NCT. | ||
|
||
The architecture is as follows: | ||
- Sunrise Stake program sends yield to a PDA owned by the Yield Router program (as at present). | ||
- Yield Router owns a [State account](https://solscan.io/account/6Uad9j9DpKE9Jhebb5T3vWNWuCYTP7XxG6LJBPaqJB31), | ||
which contains a list of destinations and their allocation percentage | ||
- The Yield Router `allocate_yield` instruction shares yield across the destinations according to their | ||
allocation percentage. | ||
- Each destination is expected to be a PDA that then distributes the yield to the climate project | ||
according to their own rules. For example, the Offset Bridge bridges the funds to Polygon, buys | ||
NCT etc. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { YieldRouterClient } from "../client"; | ||
import { logBalance } from "./lib/util"; | ||
import { PublicKey } from "@solana/web3.js"; | ||
import BN from "bn.js"; | ||
|
||
// mainnet Sunrise | ||
const defaultSunriseStateAddress = "43m66crxGfXSJpmx5wXRoFuHubhHA1GCvtHgmHW6cM1P"; | ||
const sunriseStateAddress = new PublicKey( | ||
process.env.STATE_ADDRESS ?? defaultSunriseStateAddress | ||
); | ||
|
||
if (!process.env.AMOUNT) { | ||
throw new Error("AMOUNT env variable must be set"); | ||
} | ||
const amount = parseInt(process.env.AMOUNT, 10); | ||
|
||
(async () => { | ||
const stateAddress = YieldRouterClient.getStateAddressFromSunriseAddress(sunriseStateAddress); | ||
const client = await YieldRouterClient.fetch(stateAddress); | ||
|
||
const log = logBalance(client) | ||
|
||
console.log("state address", stateAddress.toBase58()); | ||
console.log("state account data", client.config); | ||
console.log("input yield token address", client.getInputYieldAccount().toBase58()); | ||
|
||
await log("input yield token", client.getInputYieldAccount()); | ||
|
||
for (const outputYieldAccount of client.config.outputYieldAccounts) { | ||
await log("output yield token", outputYieldAccount); | ||
} | ||
|
||
console.log("Allocating yield..."); | ||
await client.allocateYield(new BN(amount)); | ||
|
||
await log("input yield token", client.getInputYieldAccount()); | ||
|
||
for (const outputYieldAccount of client.config.outputYieldAccounts) { | ||
await log("output yield token", outputYieldAccount); | ||
} | ||
|
||
|
||
|
||
})().catch(console.error); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import "./lib/util"; | ||
import {logBalance} from "./lib/util"; | ||
import { YieldRouterClient } from "../client"; | ||
import { PublicKey } from "@solana/web3.js"; | ||
|
||
// mainnet Sunrise | ||
const defaultSunriseStateAddress = "43m66crxGfXSJpmx5wXRoFuHubhHA1GCvtHgmHW6cM1P"; | ||
const sunriseStateAddress = new PublicKey( | ||
process.env.STATE_ADDRESS ?? defaultSunriseStateAddress | ||
); | ||
|
||
(async () => { | ||
const stateAddress = YieldRouterClient.getStateAddressFromSunriseAddress(sunriseStateAddress); | ||
const client = await YieldRouterClient.fetch(stateAddress); | ||
const log = logBalance(client) | ||
|
||
console.log("state address", stateAddress.toBase58()); | ||
console.log("state account data", client.config); | ||
console.log("input yield token address", client.getInputYieldAccount().toBase58()); | ||
|
||
await log("input yield token", client.getInputYieldAccount()); | ||
|
||
for (const outputYieldAccount of client.config.outputYieldAccounts) { | ||
await log("output yield token", outputYieldAccount); | ||
} | ||
})().catch(console.error); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import os from "os"; | ||
import { PublicKey, Cluster, clusterApiUrl } from "@solana/web3.js"; | ||
import {YieldRouterClient} from "../../client"; | ||
|
||
export const idWallet = os.homedir() + "/.config/solana/id.json"; | ||
|
||
process.env.ANCHOR_PROVIDER_URL = | ||
process.env.ANCHOR_PROVIDER_URL ?? clusterApiUrl((process.env.REACT_APP_SOLANA_NETWORK || 'devnet') as Cluster); | ||
process.env.ANCHOR_WALLET = process.env.ANCHOR_WALLET ?? idWallet; | ||
|
||
export const logBalance = (client: YieldRouterClient) => async (prefix: string, account: PublicKey) => { | ||
const accountInfo = await client.provider.connection.getAccountInfo(account); | ||
console.log(`${prefix} balance`, accountInfo?.lamports); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { YieldRouterClient } from "../client"; | ||
import {Keypair, PublicKey} from "@solana/web3.js"; | ||
import BN from "bn.js"; | ||
|
||
// mainnet Sunrise | ||
const defaultSunriseStateAddress = "43m66crxGfXSJpmx5wXRoFuHubhHA1GCvtHgmHW6cM1P"; | ||
const sunriseStateAddress = new PublicKey( | ||
process.env.STATE_ADDRESS ?? defaultSunriseStateAddress | ||
); | ||
|
||
// mainnet offset bridge wrapped SOL ATA | ||
const defaultOutputYieldAddress = "4XTLzYF3kteTbb3a9NYYjeDAYwNoEGSkjoqJYkiLCnmm"; | ||
const outputYieldAddress = new PublicKey( | ||
process.env.OUTPUT_YIELD_ADDRESS ?? defaultOutputYieldAddress | ||
); | ||
|
||
const anchorWallet = Keypair.fromSecretKey(Buffer.from(require(process.env.ANCHOR_WALLET as string))); | ||
|
||
(async () => { | ||
const state = await YieldRouterClient.register( | ||
sunriseStateAddress, | ||
anchorWallet.publicKey, | ||
[outputYieldAddress], | ||
[100], | ||
new BN(100) | ||
); | ||
console.log("state account data", state.config); | ||
})().catch(console.error); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { | ||
approveChecked, | ||
getAssociatedTokenAddressSync, | ||
} from "@solana/spl-token"; | ||
import { setUpAnchor } from "../client"; | ||
import { PublicKey } from "@solana/web3.js"; | ||
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; | ||
|
||
const defaultMint = "tnct1RC5jg94CJLpiTZc2A2d98MP1Civjh7o6ShmTP6"; | ||
const mint = new PublicKey(process.env.MINT ?? defaultMint); | ||
|
||
const defaultStateAddress = "CaFanGeqN6ykNTGTE7U2StJ8n1RJY6on6FoDFeLxabia"; | ||
const stateAddress = new PublicKey( | ||
process.env.STATE_ADDRESS ?? defaultStateAddress | ||
); | ||
|
||
const defaultHoldingAccount = "A4c5nctuNSN7jTsjDahv6bAWthmUzmXi3yBocvLYM4Bz"; | ||
const holdingAccount = new PublicKey( | ||
process.env.HOLDING_ACCOUNT ?? defaultHoldingAccount | ||
); | ||
|
||
(async () => { | ||
const provider = setUpAnchor(); | ||
const holdingTokenAccount = getAssociatedTokenAddressSync( | ||
mint, | ||
holdingAccount, | ||
true | ||
); | ||
|
||
await approveChecked( | ||
provider.connection, | ||
(provider.wallet as NodeWallet).payer, | ||
mint, | ||
holdingTokenAccount, | ||
stateAddress, | ||
holdingAccount, | ||
10000 * 10 ** 9, | ||
9 | ||
); | ||
})().catch(console.error); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { BuyBurnFixedClient } from "../client"; | ||
import * as anchor from "@coral-xyz/anchor"; | ||
import { PublicKey } from "@solana/web3.js"; | ||
import { getAssociatedTokenAddressSync } from "@solana/spl-token"; | ||
|
||
/** Adjust these values to whatever you want them to be */ | ||
const PRICE = 1; | ||
const PURCHASE_THRESHOLD = 100; | ||
const PURCHASE_PROPORTION = 0; | ||
|
||
const defaultTreasuryKey = "ALhQPLkXvbLKsH5Bm9TC3CTabKFSmnXFmzjqpTXYBPpu"; | ||
const treasuryKey = new PublicKey( | ||
process.env.TREASURY_KEY ?? defaultTreasuryKey | ||
); | ||
|
||
const defaultAuthority = "5HnwQGT79JypiAdjdjsXEn1EMD2AsRVVubqDyWfyWXRv"; | ||
const authorityKey = new PublicKey( | ||
process.env.AUTHORITY_KEY ?? defaultAuthority | ||
); | ||
|
||
// used in devnet | ||
const defaultMint = "tnct1RC5jg94CJLpiTZc2A2d98MP1Civjh7o6ShmTP6"; | ||
const mint = new PublicKey(process.env.MINT ?? defaultMint); | ||
|
||
const defaultHoldingAccount = "A4c5nctuNSN7jTsjDahv6bAWthmUzmXi3yBocvLYM4Bz"; | ||
const holdingAccount = new PublicKey( | ||
process.env.HOLDING_ACCOUNT ?? defaultHoldingAccount | ||
); | ||
|
||
// used for devnet testing | ||
const defaultStateAddress = "9QxfwoxkgxE94uoHd3ZPFLmfNhewoFe3Xg5gwgtShYnn"; | ||
const stateAddress = new PublicKey( | ||
process.env.STATE_ADDRESS ?? defaultStateAddress | ||
); | ||
|
||
(async () => { | ||
// get token account account for holding account | ||
const holdingAccountTokenAddress = getAssociatedTokenAddressSync( | ||
mint, | ||
holdingAccount, | ||
true | ||
); | ||
|
||
const client = await BuyBurnFixedClient.updateController( | ||
stateAddress, | ||
authorityKey, | ||
treasuryKey, | ||
mint, | ||
holdingAccount, | ||
holdingAccountTokenAddress, | ||
new anchor.BN(PRICE), | ||
PURCHASE_PROPORTION, | ||
new anchor.BN(PURCHASE_THRESHOLD) | ||
); | ||
|
||
console.log("updated state:", client.stateAddress); | ||
})().catch(console.error); |