Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
116 changes: 116 additions & 0 deletions contracts/xGNANA.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;

/*
______ ______
/ \ / \
| ▓▓▓▓▓▓\ ______ ______ | ▓▓▓▓▓▓\__ __ __ ______ ______
| ▓▓__| ▓▓/ \ / \| ▓▓___\▓▓ \ | \ | \| \ / \
| ▓▓ ▓▓ ▓▓▓▓▓▓\ ▓▓▓▓▓▓\\▓▓ \| ▓▓ | ▓▓ | ▓▓ \▓▓▓▓▓▓\ ▓▓▓▓▓▓\
| ▓▓▓▓▓▓▓▓ ▓▓ | ▓▓ ▓▓ ▓▓_\▓▓▓▓▓▓\ ▓▓ | ▓▓ | ▓▓/ ▓▓ ▓▓ | ▓▓
| ▓▓ | ▓▓ ▓▓__/ ▓▓ ▓▓▓▓▓▓▓▓ \__| ▓▓ ▓▓_/ ▓▓_/ ▓▓ ▓▓▓▓▓▓▓ ▓▓__/ ▓▓
| ▓▓ | ▓▓ ▓▓ ▓▓\▓▓ \\▓▓ ▓▓\▓▓ ▓▓ ▓▓\▓▓ ▓▓ ▓▓ ▓▓
\▓▓ \▓▓ ▓▓▓▓▓▓▓ \▓▓▓▓▓▓▓ \▓▓▓▓▓▓ \▓▓▓▓▓\▓▓▓▓ \▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓
| ▓▓ | ▓▓
| ▓▓ | ▓▓
\▓▓ \▓▓
* App: https://ApeSwap.finance
* Medium: https://ape-swap.medium.com
* Twitter: https://twitter.com/ape_swap
* Telegram: https://t.me/ape_swap
* Announcements: https://t.me/ape_swap_news
* Reddit: https://reddit.com/r/ApeSwap
* Instagram: https://instagram.com/ApeSwap.finance
* GitHub: https://github.com/ApeSwapFinance
*/

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";

interface IxToken {
event Redeem(address indexed to, uint256 xGnanaAmount, uint256 gnanaAmount);
event Deposit(address indexed from, uint256 xGnanaAmount, uint256 gnanaActualAmount);

function exchangeRate() external returns (uint256);
function redeem(uint256 amount) external;
function deposit(uint256 amount) external;
}

contract xGNANA is IxToken, ERC20, ERC20Permit, ERC20Votes {
using SafeERC20 for ERC20;

ERC20 public immutable gnana;
mapping(address => address) private _delegates;


constructor(ERC20 gnana_) ERC20("X Golden Banana", "xGNANA") ERC20Permit("X Golden Banana") {
gnana = gnana_;
}

function gnanaBalance() public view returns (uint256) {
return gnana.balanceOf(address(this));
}

function exchangeRate() public view returns (uint256) {
return gnanaBalance() * 1e18 / totalSupply();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if totalSupply() == 0, which it is in the beginning, will keep the exchange rate at 0 (or just error even actually).
A simple check on this returning 1 should fix it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch! Definitely need to update that.

}

function redeem(uint256 xGnanaAmount) external {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think redeem is more fitting but withdraw is used in a lot of places so just worth a mention

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sushi and SpookySwap use enter and leave instead of deposit and redeem/withdraw.
I think I prefer deposit and redeem but maybe worth a little vote.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to explore this more.

uint256 gnanaAmount = (xGnanaAmount * exchangeRate()) / 1e18;
_burn(msg.sender, xGnanaAmount);
gnana.safeTransfer(msg.sender, gnanaAmount);
emit Redeem(msg.sender, xGnanaAmount, gnanaAmount);
}

function deposit(uint256 gnanaGrossAmount) external {
uint256 currentExchangeRate = exchangeRate();
uint256 gnanaActualAmount = _transferGnanaFrom(msg.sender, gnanaGrossAmount);

uint256 xGnanaAmount = (gnanaActualAmount * 1e18) / currentExchangeRate;
_mint(msg.sender, xGnanaAmount);
emit Deposit(msg.sender, xGnanaAmount, gnanaActualAmount);
}

function _transferGnanaFrom(address from, uint256 gnanaGrossAmount) internal returns (uint256 gnanaActualAmount) {
uint256 previousGnanaBalance = gnanaBalance();
gnana.safeTransferFrom(from, address(this), gnanaGrossAmount);
gnanaActualAmount = gnanaBalance() - previousGnanaBalance;
}

/**
* @dev Override _transfer and prevent transferability.
*/
function _transfer(
address from,
address to,
uint256 amount
) internal pure override(ERC20) {
revert("xGNANA is non-transferrable");
}

// The following functions are overrides required by Solidity.

function _afterTokenTransfer(address from, address to, uint256 amount)
internal
virtual
override(ERC20, ERC20Votes)
{
super._afterTokenTransfer(from, to, amount);
}

function _mint(address to, uint256 amount)
internal
override(ERC20, ERC20Votes)
{
super._mint(to, amount);
}

function _burn(address account, uint256 amount)
internal
override(ERC20, ERC20Votes)
{
super._burn(account, amount);
}
}
155 changes: 155 additions & 0 deletions contracts/xGNANABribes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;

/*
______ ______
/ \ / \
| ▓▓▓▓▓▓\ ______ ______ | ▓▓▓▓▓▓\__ __ __ ______ ______
| ▓▓__| ▓▓/ \ / \| ▓▓___\▓▓ \ | \ | \| \ / \
| ▓▓ ▓▓ ▓▓▓▓▓▓\ ▓▓▓▓▓▓\\▓▓ \| ▓▓ | ▓▓ | ▓▓ \▓▓▓▓▓▓\ ▓▓▓▓▓▓\
| ▓▓▓▓▓▓▓▓ ▓▓ | ▓▓ ▓▓ ▓▓_\▓▓▓▓▓▓\ ▓▓ | ▓▓ | ▓▓/ ▓▓ ▓▓ | ▓▓
| ▓▓ | ▓▓ ▓▓__/ ▓▓ ▓▓▓▓▓▓▓▓ \__| ▓▓ ▓▓_/ ▓▓_/ ▓▓ ▓▓▓▓▓▓▓ ▓▓__/ ▓▓
| ▓▓ | ▓▓ ▓▓ ▓▓\▓▓ \\▓▓ ▓▓\▓▓ ▓▓ ▓▓\▓▓ ▓▓ ▓▓ ▓▓
\▓▓ \▓▓ ▓▓▓▓▓▓▓ \▓▓▓▓▓▓▓ \▓▓▓▓▓▓ \▓▓▓▓▓\▓▓▓▓ \▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓
| ▓▓ | ▓▓
| ▓▓ | ▓▓
\▓▓ \▓▓
* App: https://ApeSwap.finance
* Medium: https://ape-swap.medium.com
* Twitter: https://twitter.com/ape_swap
* Telegram: https://t.me/ape_swap
* Announcements: https://t.me/ape_swap_news
* Reddit: https://reddit.com/r/ApeSwap
* Instagram: https://instagram.com/ApeSwap.finance
* GitHub: https://github.com/ApeSwapFinance
*/

import "./xGNANA.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "@openzeppelin/contracts/access/AccessControlEnumerable.sol";

/**
// TODO: Possible features
- [x] AccessControl to be able to whitelist IERC20VotesDelegatee from factory contracts
- [ ] Factory contract which can deploy bribe contracts with the callback functions below
- [ ] Relevant events
- [ ] NatSpec comments
- [ ] Some ability to accumulate non GNANA revenue
- I would say that my argument still applies that we should perform buy backs at strategic points when there is high value
Is there a way we could mix in like stable revenue and with GNANA for xGNANA holders?

- [x] Provide an admin role to be able to add new white listers
- [x] Be able to remove delegatee contracts
*/

import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
interface IERC20VotesDelegatee is IERC165 {
function syncDelegatePower(address delegator) external;
}

/**
* The intent here is that a factory contract could be used to deploy bridge contracts
* where a protocol can fill up with tokens and distribute tokens to xGNANA holders who
* delegate their voting power.
*/
interface IxTokenWithBribes {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe IxTokenDelegatorCallback is more generic and "bribes" would still fall under this, but of course the idea is to be extensible and allow many other options.

event CallbackDelegateRegistered(IERC20VotesDelegatee indexed delegatee);
event CallbackDelegateRemoved(IERC20VotesDelegatee indexed delegatee);
function syncDelegateVotingPower(address delegator, address delegatee, bool withRevert) external;
function registerCallbackDelegatee(IERC20VotesDelegatee delegateeContract) external;
function removeCallbackDelegatee(IERC20VotesDelegatee delegateeContract) external;
function getVotesDelegatedTo(address delegator, address delegatee) external returns (uint256);
}

contract xGNANABribes is IxTokenWithBribes, xGNANA, AccessControlEnumerable {
// NOTE: example https://github.com/ApeSwapFinance/apeswap-pool-factory/blob/f24fb874a9f70cedcfadeecd4a6ddacb45ee9752/contracts/PoolManager.sol
using EnumerableSet for EnumerableSet.AddressSet;

EnumerableSet.AddressSet private _registeredCallbackDelegatees;

bytes32 public constant DELEGATEE_MANAGER_ROLE = keccak256("DELEGATEE_MANAGER_ROLE");

constructor(ERC20 gnana_, address admin_) xGNANA(gnana_) {
_grantRole(DEFAULT_ADMIN_ROLE, admin_);
_setRoleAdmin(DELEGATEE_MANAGER_ROLE, DEFAULT_ADMIN_ROLE);
}

function syncDelegateVotingPower(address delegator, address delegatee, bool withRevert) public {
bool synced = _syncDelegateVotingPower(delegator, delegatee);
if(withRevert && !synced) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably turn this internal and provide a public function which automatically reverts if the delegatee is not registered.

revert("delegatee not registered");
}
}

function getRegisteredCallbackDelegateesCount() external view returns (uint256) {
return _registeredCallbackDelegatees.length();
}

function getRegisteredCallbackDelegateesAt(uint256 index) external view returns (address) {
return _registeredCallbackDelegatees.at(index);
}

function getAllRegisteredCallbackDelegatees() external view returns (address[] memory) {
return _registeredCallbackDelegatees.values();
}

function registerCallbackDelegatee(IERC20VotesDelegatee callbackDelegatee) external override onlyRole(DELEGATEE_MANAGER_ROLE) {
require(callbackDelegatee.supportsInterface(type(IERC20VotesDelegatee).interfaceId), "Does not support interface");
if(!_registeredCallbackDelegatees.contains(address(callbackDelegatee))) {
_registeredCallbackDelegatees.add(address(callbackDelegatee));
emit CallbackDelegateRegistered(callbackDelegatee);
}
}

function removeCallbackDelegatee(IERC20VotesDelegatee callbackDelegatee) external override onlyRole(DEFAULT_ADMIN_ROLE) {
require(_registeredCallbackDelegatees.contains(address(callbackDelegatee)), "not registered");
_registeredCallbackDelegatees.remove(address(callbackDelegatee));
emit CallbackDelegateRemoved(callbackDelegatee);
}

function getVotesDelegatedTo(address delegator, address delegatee) external view override returns (uint256) {
address currentDelegatee = delegates(delegator);
if(currentDelegatee == delegatee) {
return balanceOf(delegator);
} else {
return 0;
}
}

/**
* @dev This hook is called after _mint and _burn even though
* xGNANA is non-transferrable
*/
function _afterTokenTransfer(
address from,
address to,
uint256 amount
) internal override(xGNANA) {
super._afterTokenTransfer(from, to, amount);
// After balances are updated, sync external
_syncDelegateVotingPower(delegates(from), from);
_syncDelegateVotingPower(delegates(to), to);
}

/**
* @dev Change delegation for `delegator` to `delegatee`.
*
* Sync delegation power with external delegation contract for bribes.
*/
function _delegate(address delegator, address delegatee) internal override(ERC20Votes) {
address fromDelegate = delegates(delegator);
super._delegate(delegator, delegatee);

_syncDelegateVotingPower(fromDelegate, delegator);
_syncDelegateVotingPower(delegatee, delegator);
}

function _syncDelegateVotingPower(address delegator, address delegatee) internal returns (bool){
if(_registeredCallbackDelegatees.contains(delegatee)) {
IERC20VotesDelegatee(delegatee).syncDelegatePower(delegator);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tricky part is that an external contract could cause a denial of service if they revert inside this function.

The whitelist is important for that, but may not catch the edge cases. Paladin strongly recommended against using a try/catch as there is a way to purposely throw it which could allow users to unstake xGNANA and still have bribe rewards on another contract.

The nice thing is that worst case we can de-whitelist a delagatee contract and it would allow xGNANA to be redeemed again.

return true;
} else {
return false;
}
}
}